2025/02/14

使用 Python Azure SDK 連接 Azure Blob Storage

首先輸入下方指令安裝套件
pip install azure-storage-blob python-dotenv azure-core

配置檔如下:
AZURE_CONNECTION_STRING=
AZURE_CONTAINER_NAME=
代碼如下:
import argparse
import os
from typing import Optional

from azure.storage.blob import BlobServiceClient, ContainerClient, BlobClient
from dotenv import load_dotenv

# 載入.env
load_dotenv()

# 設定連接字串
AZURE_CONNECTION_STRING = os.getenv("AZURE_CONNECTION_STRING", None)
# 設定容器名稱
AZURE_CONTAINER_NAME = os.getenv("AZURE_CONTAINER_NAME", None)

# 如果不存在就拋出錯誤
if not AZURE_CONNECTION_STRING:
    raise ValueError("Azure connection string is not provided.")

# 全域變數 BlobServiceClient
azure_blob_service_client = BlobServiceClient.from_connection_string(AZURE_CONNECTION_STRING)


def get_container_client(container_name: str = AZURE_CONTAINER_NAME) -> Optional[ContainerClient]:
    """
    取得指定容器的 ContainerClient,如果容器不存在則建立新容器

    Args:
        container_name: 容器名稱,如果未指定則使用環境變數中的設定

    Returns:
        ContainerClient: 成功返回容器客戶端對象
        None: 如果發生錯誤
    """
    # 取得container client
    client = azure_blob_service_client.get_container_client(AZURE_CONTAINER_NAME.lower())
    # 如果存在就回傳
    if client.exists():
        return client
    else:
        # 如果不存在就建立
        return create_container(container_name)


def create_container(container_name: str = None) -> Optional[ContainerClient]:
    """
    建立新的 Blob 存儲容器,如果容器已存在則返回現有容器

    Args:
        container_name: 要建立的容器名稱

    Returns:
        ContainerClient: 成功返回容器客戶端對象
        None: 如果發生錯誤

    Raises:
        ValueError: 未提供容器名稱時拋出
    """
    global AZURE_CONTAINER_NAME

    # 如果名稱不存在就拋出錯誤
    if not container_name:
        raise ValueError("Container name is not provided.")

    try:
        # 將容器名稱轉為小寫
        container_name_lower = container_name.lower()

        # 直接檢查容器是否存在
        container_client = azure_blob_service_client.get_container_client(container_name_lower)
        if container_client.exists():
            return container_client

        # 不存在則建立新容器
        AZURE_CONTAINER_NAME = container_name_lower
        print(f"Container '{AZURE_CONTAINER_NAME}' created successfully.")
        return azure_blob_service_client.create_container(AZURE_CONTAINER_NAME)
    except Exception as e:
        print(f"Error creating container: {e}")
        return None


def delete_container(container_name: str = AZURE_CONTAINER_NAME) -> bool:
    """
    刪除指定的 Blob 存儲容器

    Args:
        container_name: 要刪除的容器名稱,如果未指定則使用環境變數中的設定

    Returns:
        bool: 刪除成功返回 True,失敗返回 False

    Raises:
        ValueError: 未提供容器名稱時拋出
    """
    try:
        # 如果容器名稱不存在就拋出錯誤
        if not container_name:
            raise ValueError("Container name is not provided")

        # 直接獲取容器客戶端,而不是使用 get_container_client()
        container_client = azure_blob_service_client.get_container_client(container_name.lower())

        # 如果容器不存在就回傳
        if not container_client.exists():
            print(f"Container '{container_name}' does not exist")
            return False
        # 刪除容器
        container_client.delete_container()
        print(f"Container '{container_name}' deleted successfully")
        return True
    except Exception as e:
        print(f"Error deleting container: {e}")
        return False


def get_blob_client(blob_name: str) -> BlobClient:
    """
    取得指定 Blob 的客戶端對象

    Args:
        blob_name: Blob 的名稱

    Returns:
        BlobClient: Blob 客戶端對象

    Raises:
        ValueError: 容器客戶端不可用時拋出
    """
    container_client = get_container_client()
    if not container_client:
        raise ValueError("Container client not available")

    return container_client.get_blob_client(blob_name)


def upload_blob(file_path: str) -> None:
    """
    上傳檔案到 Blob 存儲

    Args:
        file_path: 要上傳的檔案路徑(支持相對和絕對路徑)

    Returns:
        None

    Raises:
        FileNotFoundError: 檔案不存在時拋出
        ValueError: 容器客戶端不可用時拋出
    """
    try:
        # 開啟檔案
        with open(file_path, "rb") as data:
            # 取得檔案名稱
            blob_name = os.path.basename(file_path)
            # 取得blob client
            blob_client = get_blob_client(blob_name)
            # 上傳blob 並且覆蓋相同檔案名稱的檔案
            blob_client.upload_blob(data, overwrite=True)
        print(f"Blob '{blob_name}' uploaded successfully.")
    except Exception as e:
        print(f"Error uploading blob: {e}")


def download_blob(blob_name: str, download_path: str) -> bool:
    """
    從 Blob 存儲下載檔案

    Args:
        blob_name: 要下載的 Blob 名稱
        download_path: 下載檔案的保存路徑(會自動創建目錄)

    Returns:
        bool: 下載成功返回 True,失敗返回 False

    Raises:
        ValueError: 容器客戶端不可用時拋出
    """
    try:
        blob_client = get_blob_client(blob_name)

        # 確保下載目錄存在
        os.makedirs(os.path.dirname(os.path.abspath(download_path)), exist_ok=True)

        # 下載檔案
        with open(download_path, "wb") as download_file:
            download_file.write(blob_client.download_blob().readall())

        print(f"Blob '{blob_name}' 下載成功,保存至 '{download_path}'")
        return True

    except Exception as e:
        print(f"下載 blob 時發生錯誤: {e}")
        return False


def delete_blob(blob_name: str) -> bool:
    """
    刪除指定的 Blob

    Args:
        blob_name: 要刪除的 Blob 名稱

    Returns:
        bool: 刪除成功返回 True,失敗返回 False

    Raises:
        ValueError: 容器客戶端不可用時拋出
    """
    try:
        # 取得blob client
        blob_client = get_container_client().get_blob_client(blob_name)
        # 如果存在就刪除
        if blob_client.exists():
            # 刪除blob
            blob_client.delete_blob()
            print(f"Blob '{blob_name}' deleted successfully.")
            return True
        else:
            print(f"Blob '{blob_name}' does not exist.")
            return False
    except Exception as e:
        print(f"Error deleting blob: {e}")
        return False


def list_blobs() -> None:
    """
    列出容器中的所有 Blobs

    打印格式:
    Found X blob(s):
    - blob1_name
    - blob2_name
    ...

    Returns:
        None

    Raises:
        ValueError: 容器客戶端不可用時拋出
    """
    try:
        # 將 blobs 轉換成列表
        blob_list = list(get_container_client().list_blobs())

        # 檢查是否有 blobs
        if blob_list:
            print(f"Found {len(blob_list)} blob(s):")
            for blob in blob_list:
                print(f"- {blob.name}")
        else:
            print("No blobs found in the container.")
    except Exception as e:
        print(f"Error listing blobs: {e}")


def main():
    """
    主程式入口,處理命令行參數並執行相應操作

    支持的操作:
    - upload: 上傳檔案
    - download: 下載檔案
    - delete: 刪除 Blob
    - list: 列出所有 Blobs
    - create_container: 創建容器
    - delete_container: 刪除容器

    Returns:
        int: 0 表示成功,1 表示失敗
    """
    parser = argparse.ArgumentParser(
        description='Azure Blob Storage 操作工具',
        formatter_class=argparse.RawTextHelpFormatter
    )

    # 定義操作類型,添加 download 選項
    actions_help = """操作類型:
  upload            上傳文件到 Blob 存儲
  download          從 Blob 存儲下載文件
  delete           刪除指定的 Blob
  list             列出容器中的所有 Blobs
  create_container  創建新的存儲容器
  delete_container  刪除指定的存儲容器"""

    parser.add_argument(
        'action',
        choices=['upload', 'download', 'delete', 'list', 'create_container', 'delete_container'],
        help=actions_help
    )

    parser.add_argument(
        '--file',
        metavar='FILE_PATH',
        help='文件路徑(上傳時為源文件路徑,下載時為保存路徑)'
    )

    parser.add_argument(
        '--blob',
        metavar='BLOB_NAME',
        help='Blob 名稱(用於下載、刪除操作)'
    )

    parser.add_argument(
        '--container',
        metavar='CONTAINER_NAME',
        help='容器名稱(配合 create_container 或 delete_container 使用)'
    )

    args = parser.parse_args()

    try:
        if args.action == 'upload':
            if not args.file:
                raise ValueError("上傳操作需要提供文件路徑 (--file)")
            upload_blob(args.file)

        elif args.action == 'download':
            if not args.blob or not args.file:
                raise ValueError("下載操作需要提供 Blob 名稱 (--blob) 和保存路徑 (--file)")
            download_blob(args.blob, args.file)

        elif args.action == 'delete':
            if not args.blob:
                raise ValueError("刪除操作需要提供 Blob 名稱 (--blob)")
            delete_blob(args.blob)

        elif args.action == 'list':
            list_blobs()

        elif args.action == 'create_container':
            if not args.container:
                raise ValueError("創建容器操作需要提供容器名稱 (--container)")
            create_container(args.container)

        elif args.action == 'delete_container':
            container_name = args.container if args.container else AZURE_CONTAINER_NAME
            delete_container(container_name)

    except Exception as e:
        print(f"Error: {e}")
        return 1

    return 0


if __name__ == '__main__':
    exit(main())