2011年5月31日火曜日

Azure Blob ストレージへ REST API にてアップロードするアクティビティ(仮)

ストレージ関係の REST API を暇を見つけながらちろちろやっている最中ですが、とりあえずアップロード(ただし条件付き)ができるところまではできましたので、恒例のアクティビティ化をしてみました。
条件付き、というのは本来ストレージへアップロードを行う場合には何点か考慮しなくてはいけないポイントがあり、現時点ではその考慮点の一部にしか対応できていないためです。何を考える必要があるかと言うと、次の点になります。

  • アップロードするファイルの容量

Windows Azure のストレージには大きくわけて 2 つの方式が提供されています。一つは BlockBlob と呼ばれる種類で、これは 64MB までの一つのファイル、または 4MB 単位に分割したファイル群という形でアップロードが可能です。分割するメリットは同時に複数セッションを利用してアップロード速度を向上させたり、エラー発生時のリトライ処理量を減らすなどがあたります。もう一つは PageBlob と呼ばれる種類でこちらは 1TB までのファイルを扱う事が可能な点と、バイト単位(イメージとしてはHDD等のセクタ)にて読み書きの処理が行える点がポイントとなります。現在ベータ版として提供されている Azure Drive (ストレージを通常の NTFS ドライブに見せかけて利用できる)は PageBlob をベースに作成されていると思われます。


これらの仕様があるので、アップロードするファイル容量により BlockBlob か PageBlob かを分ける必要がまず発生します。そして BlockBlob を利用する際はどのような単位でブロックを区切るか、ブロックを束ねるためのブロックリストの作成、といった追加処理が必要となります。

今回はまだ実装途中というのもあり、単純に指定したファイルを BlockBlob に分割なしでアップロードするところまでになっています。REST API としては PutBlobPutBlockPutBlockList とそれぞれ別の API を利用する事になりますが、アクティビティとして考えた場合これらは1つにまとめられていなければ使い勝手が悪いと考えられますので、最終的には全てをまとめた形にまで実装する予定です。

長くなりますが一気にソースを掲載します。

   1: Imports System.ComponentModel
   2: Imports System.Activities.Presentation.PropertyEditing
   3: Imports System.Text
   4: Imports System.Net
   5:  
   6: Public Class PutAzureBlobsActivity
   7:     Inherits AzureStorageActivityBase
   8:  
   9:     ''' <summary>1ブロックの最大サイズ</summary>
  10:     Private Const BLOCKSIZEBYTE As Long = 4194304 '4MB
  11:     ''' <summary>ブロブの1ファイル通常時最大サイズ</summary>
  12:     Private MAX_BLOBSIZE As Long = 67108864 '64MB
  13:     ''' <summary>ブロブの1ファイル最大サイズ</summary>
  14:     Private Const MAX_BLOBSIZEUSEBLOCK As Long = 214748364800 '200GB
  15:  
  16:     <Category("アップロード設定")>
  17:     <DisplayName("ページ形式を利用")>
  18:     Public Property UsePage As Boolean = False
  19:  
  20:     <Category("アップロード設定")>
  21:     <DisplayName("アップロードするファイル")>
  22:     <Editor(GetType(AnyFileBrowserDialogPropertyValueEditor), GetType(DialogPropertyValueEditor))>
  23:     Public Property UploadFilename As String
  24:     <Category("アップロード設定")>
  25:     <DisplayName("ブロブ上でのファイル名")>
  26:     Public Property BlobFileName As String
  27:  
  28:     <Category("メタデータ")>
  29:     <DisplayName("メタデータ種類名")>
  30:     Public Property MetadataName As String
  31:     <Category("メタデータ")>
  32:     <DisplayName("メタデータ値")>
  33:     Public Property MetadataValue As String
  34:  
  35:     Public Sub New()
  36:         Me.DisplayName = "Azure ブロブへアップロード"
  37:     End Sub
  38:  
  39:     Protected Overrides Sub AddRequestHeader(wq As System.Net.WebRequest)
  40:         Dim content As String = Me.GetContentType
  41:         wq.ContentType = content
  42:         wq.Headers.Add("x-ms-blob-content-type", content)
  43:  
  44:         '対象ファイルのサイズを取得
  45:         Dim flSize As Long = My.Computer.FileSystem.GetFileInfo(Me.UploadFilename).Length
  46:         If Me.UsePage Then
  47:             'Page Blob を利用
  48:             wq.ContentLength = 0
  49:             wq.Headers.Add("x-ms-blob-content-length", flSize.ToString)
  50:             wq.Headers.Add("x-ms-blob-type", "PageBlob")
  51:         Else
  52:             'Block Blob
  53:             wq.ContentLength = flSize
  54:             wq.Headers.Add("x-ms-blob-type", "BlockBlob")
  55:         End If
  56:         'メタデータの設定
  57:         If (Me.MetadataName IsNot Nothing) AndAlso (Me.MetadataName.Trim <> "") Then
  58:             wq.Headers.Add("x-ms-meta-" + Me.MetadataName, Web.HttpUtility.UrlEncode(Me.MetadataValue))
  59:         End If
  60:  
  61:         MyBase.AddRequestHeader(wq)
  62:     End Sub
  63:  
  64:     Protected Overrides Sub SetHttpMethod(wq As System.Net.WebRequest)
  65:         wq.Method = "PUT"
  66:     End Sub
  67:  
  68:     Protected Overrides Function CreateApiPath() As System.Uri
  69:         Return New Uri(String.Format(STORAGE_API_PATH, Me.Account, Me.ResourceName) _
  70:                                      + "/" + Me.BlobFileName)
  71:     End Function
  72:  
  73:     Private Overloads Function CreateBodyMessage() As Byte()
  74:         Return My.Computer.FileSystem.ReadAllBytes(Me.UploadFilename)
  75:     End Function
  76:  
  77:     Protected Overrides Function GetWebData() As String()
  78:         Dim resultStrings As String = ""
  79:         Dim resultStatus As String = ""
  80:         Dim resultRequestID As String = ""
  81:         Dim resultHeaders As New StringBuilder
  82:         Try
  83:             Dim requestUri As Uri = Me.CreateApiPath()
  84:             Dim localWebRequest = TryCast(HttpWebRequest.Create(requestUri), HttpWebRequest)
  85:             Me.SetHttpMethod(localWebRequest)
  86:  
  87:             Dim bodyMessageBytes = Me.CreateBodyMessage()
  88:             If bodyMessageBytes IsNot Nothing Then
  89:                 localWebRequest.ContentLength = bodyMessageBytes.Length
  90:             End If
  91:             Me.AddRequestHeader(localWebRequest)
  92:  
  93:             If bodyMessageBytes IsNot Nothing Then
  94:                 Using reqStream = localWebRequest.GetRequestStream
  95:                     reqStream.Write(bodyMessageBytes, 0, bodyMessageBytes.Length)
  96:                 End Using
  97:             End If
  98:  
  99:             Using webResponse = TryCast(localWebRequest.GetResponse(), HttpWebResponse)
 100:                 For Each child In webResponse.Headers.AllKeys
 101:                     resultHeaders.Append(child + ":" + webResponse.Headers(child).ToString + ControlChars.NewLine)
 102:                 Next
 103:                 resultStatus = webResponse.StatusCode.ToString
 104:                 resultRequestID = webResponse.Headers("x-ms-request-id")
 105:                 Using responseStream = webResponse.GetResponseStream()
 106:                     Using reader As New System.IO.StreamReader(responseStream)
 107:                         resultStrings = reader.ReadToEnd
 108:                     End Using
 109:                 End Using
 110:             End Using
 111:  
 112:         Catch ex As Exception
 113:             resultStrings = ex.Message
 114:         End Try
 115:         Return New String() {resultStrings, resultStatus, resultRequestID, resultHeaders.ToString}
 116:     End Function
 117:  
 118:     ''' <summary>拡張子による Content-Type の取得</summary>
 119:     Private Function GetContentType() As String
 120:         If Not System.IO.File.Exists(Me.UploadFilename) Then Return Nothing
 121:         Dim resultType As String = ""
 122:         Dim fileExt As String = System.IO.Path.GetExtension(Me.UploadFilename)
 123:         Select Case fileExt.ToLower.Replace("."c, "")
 124:             Case "txt" : resultType = "text/plain" 'テキスト文書    .txt    text/plain
 125:             Case "csv" : resultType = "text/csv" 'CSVファイル    .csv    text/csv
 126:             Case "tsv" : resultType = "text/tab-separated-values" 'TSVファイル    .tsv    text/tab-separated-values
 127:             Case "doc", "docx" : resultType = "application/msword" 'ワード文書    .doc    application/msword
 128:             Case "xls", "xlsx" : resultType = "application/vnd.ms-excel" 'エクセルシート    .xls    application/vnd.ms-excel
 129:             Case "ppt", "pptx" : resultType = "application/vnd.ms-powerpoint" 'パワーポイント    .ppt    application/vnd.ms-powerpoint
 130:             Case "pdf" : resultType = "application/pdf" 'PDF文書    .pdf    application/pdf
 131:             Case "htm", "html" : resultType = "text/html" 'HTML文書    .html .htm    text/html
 132:             Case "css" : resultType = "text/css" 'スタイルシート    .css    text/css
 133:             Case "js" : resultType = "text/javascript" 'JavaScriptファイル    .js    text/javascript
 134:             Case "jpg", "jpeg" : resultType = "image/jpeg" 'JPEG    .jpg .jpeg    image/jpeg
 135:             Case "png" : resultType = "image/png" 'PNG    .png    image/png
 136:             Case "gif" : resultType = "image/gif" 'GIF    .gif    image/gif
 137:             Case "bmp" : resultType = "image/bmp" 'ビットマップ    .bmp    image/bmp
 138:             Case "mp3" : resultType = "audio/mpeg" 'MP3    .mp3    audio/mpeg
 139:             Case "mp4" : resultType = "audio/mp4" 'MP4    .m4a    audio/mp4
 140:             Case "wav" : resultType = "audio/x-wav" 'WAV    .wav    audio/x-wav
 141:             Case "midi" : resultType = "audio/midi" 'MIDI    .mid .midi    audio/midi
 142:             Case "mpeg" : resultType = "video/mpeg" 'MPEG    .mpg .mpeg    video/mpeg
 143:             Case "wmv" : resultType = "video/x-ms-wmv" 'WMV    .wmv    video/x-ms-wmv
 144:             Case "swf" : resultType = "application/x-shockwave-flash" 'Flash (Shockwave)    .swf    application/x-shockwave-flash
 145:             Case "3g2" : resultType = "video/3gpp2" '3GPP2    .3g2    video/3gpp2
 146:             Case "zip" : resultType = "application/zip" 'ZIP形式    .zip    application/zip
 147:             Case "lzh", "lha" : resultType = "application/x-lzh" 'LZH形式    .lha .lzh    application/x-lzh
 148:             Case "tar", "tgz" : resultType = "application/x-tar" 'tar / tar+gzip形式    .tar .tgz    application/x-tar
 149:             Case "exe", "com" : resultType = "application/octet-stream" '実行ファイル    .exe    application/octet-stream
 150:             Case Else : resultType = "application/octet-stream"
 151:         End Select
 152:         Return resultType
 153:     End Function
 154:  
 155: End Class

※ベースとなるクラスは過去の記事にて紹介したソース一式に含まれています。
実装上のポイントは次の点です。

  • リクエストヘッダにて x-ms-blob-type で Blob の種類を指定する必要がある

実際にはソース上で指定している、x-ms-blob-content-type とか x-ms-blob-content-length とかは必須項目ではないので省略しても構わないのですが、ブラウザから参照する際など Content-Type が指定されていなければ色々と問題がありますので、できる限りは指定するようにしています。その際の Content-Type 判定ですが、拡張子から行う方法を採っています。楽なのでw

その点以外には特に難しいポイントはなく、HttpWebRequest から RequestStorm を利用してバイナリ形式で書き込んであげればアップロードは完了です。このあたりは webEDI とかを扱った事のある方でしたら特に悩むこともないと思います。

PutAzureBlobBeta1

ワークフロー上で利用する場合はこのような形で設定します。これだけで Azure のストレージにファイルがアップロードできます。

PutAzureBlobBeta1-1

実行後に Storage Explorer にて実際のストレージの中身を確認すると上記のように表示されます。

PutAzureBlobBeta1-2

ちゃんと Content-Type も指定されていますので、ブラウザで直接アクセスを行った場合も問題なく扱われています。

0 件のコメント:

コメントを投稿