仕事で、バックアップファイルをAzure Blob Storageにアップロードさせるスクリプトを作成する必要が出てきた。
Azure Blob StorageはRest APIに対応しているので、それで上げれば良さそうだ。

というわけで、こちらの内容を参考にPythonに書き直してスクリプトを作成してみた。
運用時にアップロード・ダウンロード・削除と使い分けするのが面倒だったので、サブコマンドを指定する方式にしている。

azure_blob_backup.py

#!/bin/python
# -*- coding: utf-8 -*-
# +---------------------------------------------------------------------------------------+
# + [作成日] : 2016/02/02                                                                 +
# + [作成者] : Blacknon                                                                   +
# + [概要] :                                                                              +
# + AzureのBlobに対し、ファイルのアップロード・ダウンロード・削除処理を実施するスクリプト +
# +---------------------------------------------------------------------------------------+
import pycurl
import urllib
import datetime
import base64
import hmac,hashlib
import argparse
import os.path
import cStringIO
import xml.etree.ElementTree as ET

## --------------
# アカウント情報
## --------------
blob_account = 'ストレージアカウント'
blob_accesskey = 'アクセスキー'

def azure_blob_access():
    # 変数の代入
    blob_container = args.blob_container
    if args.subcommand == 'upload':
        file_path = args.file_path
        file_name = os.path.basename(file_path)
        http_request = 'PUT'
        html_body = open(file_path).read()
        html_size = str(len(html_body))
        content_leng = "Content-Length:" + html_size
        blob_path = "/" + blob_account + "/" + blob_container + "/" + file_name
        blob_url = "https://" + blob_account +".blob.core.windows.net/" + blob_container + "/" + file_name
    elif args.subcommand == 'download':
        file_name = args.file_name
        http_request = 'GET'
        if os.path.isdir(args.output_path):
            output_path = args.output_path + "/" + file_name
        else:
            output_path = args.output_path
        print output_path
        html_size = ''
        content_leng = ''
        blob_path = "/" + blob_account + "/" + blob_container + "/" + file_name
        blob_url = "https://" + blob_account +".blob.core.windows.net/" + blob_container + "/" + file_name
    elif args.subcommand == 'delete':
        file_name = args.file_name
        http_request = 'DELETE'
        html_size = ''
        content_leng = ''
        blob_path = "/" + blob_account + "/" + blob_container + "/" + file_name
        blob_url = "https://" + blob_account +".blob.core.windows.net/" + blob_container + "/" + file_name
    elif args.subcommand == 'list':
        http_request = 'GET'
        html_size = ''
        content_leng = ''
        blob_path = "/" + blob_account + "/" + blob_container + '\ncomp:list\nrestype:container'
        blob_url = "https://" + blob_account +".blob.core.windows.net/" + blob_container + '?restype=container&comp=list'

    # HTTPリクエスト情報の作成
    file_type = 'text/plain'
    html_headers = [
                    'x-ms-blob-type:BlockBlob',
                    'x-ms-version:2014-02-14',
                    ]
    html_date = datetime.datetime.now().strftime("%a, %d %b %Y %H:%M:%S GMT")
    stringToSign    = [
                      # VERB
                      http_request,
                      # Content-Encoding
                      '',
                      # Content-Language
                      '',
                      # Content-Length
                      html_size,
                      # Content-MD5
                      '',
                      # Content-Type
                      file_type,
                      # Date
                      html_date,
                      # If-Modified-Since
                      '',
                      # If-Match
                      '',
                      # If-None-Match
                      '',
                      # If-Unmodified-Since
                      '',
                      # Range
                      '',
                      ]
    stringToSign = stringToSign + html_headers + [blob_path]
    stringToSign = "\n".join(stringToSign)

    signature = base64.encodestring(hmac.new(base64.decodestring(blob_accesskey),stringToSign,hashlib.sha256).digest())
    signature = signature.rstrip("\n")
    authorization = "SharedKey " + blob_account +":" + signature

    html_headers = html_headers + [
                   "Authorization:" + authorization,
                   "Date:" + html_date,
                   "Content-Type:" + file_type,
                   content_leng,
                   ]
    c = pycurl.Curl()
    c.setopt(pycurl.URL, blob_url.rstrip("\n"))
    c.setopt(pycurl.HTTPHEADER, html_headers)
    c.setopt(pycurl.CUSTOMREQUEST, http_request)
    if args.subcommand == 'upload':
        c.setopt(pycurl.POSTFIELDS, html_body)
    elif args.subcommand == 'download':
        op = open(output_path,'wb')
        c.setopt(pycurl.WRITEDATA, op)
    elif args.subcommand == 'list':
        response = cStringIO.StringIO()
        c.setopt(c.WRITEFUNCTION, response.write)

    c.perform()
    if args.subcommand == 'list':
        text = response.getvalue()
        root = ET.fromstring(text)
        ufilelist= root.findall(".//Name")
        for ufile in ufilelist:
            print(ufile.text);

## ------------
# Paser設定
## ------------
parser = argparse.ArgumentParser(description='Azure Blob File Upload/Download/Delete. Only 1 File.')
subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand')

# upload
parser_up = subparsers.add_parser('upload',help='upload file to Azure Blob.')
parser_up.add_argument('-c','--container',type=str,dest='blob_container',required=True,help='File upload destination of the Blob Container')
parser_up.add_argument('-f','--upload_file',type=str,dest='file_path',required=True,help='File to be uploaded to the Blob Container')

# download
parser_down = subparsers.add_parser('download',help='download file from Azure Blob.')
parser_down.add_argument('-c','--container',type=str,dest='blob_container',required=True,help='File download from This Blob Container')
parser_down.add_argument('-f','--download_file',type=str,dest='file_name',required=True,help='File to be downloaded to the Blob Container')
parser_down.add_argument('-o','--output_path',type=str,dest='output_path',required=True,help='File to out put path')

# delete
parser_del = subparsers.add_parser('delete',help='delete file Azure Blob.')
parser_del.add_argument('-c','--container',type=str,dest='blob_container',required=True,help='File delete from This Blob Container')
parser_del.add_argument('-f','--delete_file',type=str,dest='file_name',required=True,help='Delete to the Blob Container')

# list
parser_list = subparsers.add_parser('list',help='list up Azure Blob file.')
parser_list.add_argument('-c','--container',type=str,dest='blob_container',required=True,help='Get file list from This Blob Container')

# 変数代入
args=parser.parse_args()

azure_blob_access()

サブコマンドでlist、upload、download、deleteを使い、Azure Blob Storageに接続、操作できるようにしている。
なお、以下の問題点があるんだけど、とりあえず今は直してない。。。

  • Azure Blob Storage上にないファイルをダウンロードすると、エラーにならないでアウトプットに指定したPATHにそのまま出力する
  • Azure Blob Storageの仕様で64MBの制限があるのだが、その辺を考慮していない(上げたければ事前に分割しておく事)

ま、そのうち直す事にしようかな〜と…
今のところ、特に大きな影響ないし。