2026年2月12日木曜日

Windows のルーティング情報を可視化する Powershell

Windows のルーティング情報を可視化する Powershell

概要

タイトル通り

環境

  • Windows 11

スクリプト

# Visualize-Routes.ps1 (vis.jsを使用したHTMLベースの可視化)

param (
    [string]$OutputPath = "$($PSScriptRoot)\network_routes.html",
    [switch]$IncludeDefaultRoutes = $false,
    [string[]]$ExcludeNextHops = @()
)

function Visualize-NetRouteGraph {
    [CmdletBinding()]
    param (
        [string]$OutputPath = "$($PSScriptRoot)\network_routes.html",
        [switch]$IncludeDefaultRoutes = $false,
        [string[]]$ExcludeNextHops = @()
    )

    # ルーティング情報を取得
    $routes = Get-NetRoute
    
    # デフォルトルートを除外する場合
    if (-not $IncludeDefaultRoutes) {
        $routes = $routes | Where-Object { $_.NextHop -ne "::" -and $_.NextHop -ne "0.0.0.0" }
    }
    
    # 指定されたNextHopを除外する場合
    if ($ExcludeNextHops.Count -gt 0) {
        foreach ($excludeHop in $ExcludeNextHops) {
            $routes = $routes | Where-Object { $_.NextHop -ne $excludeHop }
        }
    }

    if (-not $routes) {
        Write-Warning "ルーティング情報が見つかりませんでした。"
        return
    }

    # ノードとエッジの定義
    $nodes = @{}
    $edges = @()
    $nodeId = 0
    $nodeMap = @{}
    $edgeMap = @{}  # エッジの重複を管理

    foreach ($route in $routes) {
        $destination = $route.DestinationPrefix
        $nextHop = $route.NextHop
        $ifIndex = $route.IfIndex
        $interface = $null
        
        # ネットワークアダプタ名を取得(エラーハンドリング付き)
        try {
            $interface = (Get-NetAdapter -IfIndex $ifIndex -ErrorAction Stop).Name
        }
        catch {
            $interface = $null
        }

        # ノードの作成(重複なし)
        if (-not $nodeMap.ContainsKey($destination)) {
            $nodeMap[$destination] = @{
                id    = $nodeId
                label = $destination
                title = "Destination: $destination"
                color = "lightblue"
                shape = "box"
            }
            $nodeId++
        }

        if (-not $nodeMap.ContainsKey($nextHop)) {
            $nodeMap[$nextHop] = @{
                id    = $nodeId
                label = $nextHop
                title = "NextHop: $nextHop"
                color = "lightgreen"
                shape = "ellipse"
            }
            $nodeId++
        }

        # エッジのキーを作成(重複チェック用)
        # NextHop → Destination の向きにすることで、トラフィックフローを表現
        $edgeKey = "$($nodeMap[$nextHop].id)->$($nodeMap[$destination].id)"
        
        # エッジラベルの作成
        $edgeInfo = "Metric: $($route.RouteMetric)"
        if ($interface) {
            $edgeInfo += " (via $interface)"
        }
        
        # 既存のエッジがあれば統合、なければ新規作成
        if ($edgeMap.ContainsKey($edgeKey)) {
            # 既存のエッジにラベルを追加
            $edgeMap[$edgeKey].labels += $edgeInfo
        }
        else {
            # 新規エッジを作成
            $edgeMap[$edgeKey] = @{
                from   = $nodeMap[$nextHop].id
                to     = $nodeMap[$destination].id
                labels = @($edgeInfo)
            }
        }
    }
    
    # エッジマップからエッジ配列を作成(ラベルを結合)
    foreach ($edgeKey in $edgeMap.Keys) {
        $edge = $edgeMap[$edgeKey]
        $edges += @{
            from  = $edge.from
            to    = $edge.to
            label = $edge.labels -join "`n"
        }
    }

    # JSON形式でノードとエッジを作成
    $nodesJson = $nodeMap.Values | ConvertTo-Json
    $edgesJson = $edges | ConvertTo-Json

    # HTMLテンプレートを生成
    $htmlContent = @"
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ネットワークルーティンググラフ</title>
    <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        html, body {
            width: 100%;
            height: 100%;
            font-family: Arial, sans-serif;
        }
        #network {
            width: 100%;
            height: 100%;
            border: 1px solid lightgray;
        }
        .controls {
            position: absolute;
            top: 10px;
            left: 10px;
            background: white;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 5px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            z-index: 100;
        }
        button {
            padding: 5px 10px;
            margin: 5px 2px;
            cursor: pointer;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
        }
        button:hover {
            background-color: #45a049;
        }
    </style>
</head>
<body>
    <div class="controls">
        <button onclick="resetView()">リセット</button>
        <button onclick="fitToScreen()">画面に合わせる</button>
    </div>
    <div id="network"></div>

    <script type="text/javascript">
        var nodes = new vis.DataSet($nodesJson);
        var edges = new vis.DataSet($edgesJson);

        var container = document.getElementById('network');
        var data = {
            nodes: nodes,
            edges: edges
        };

        var options = {
            physics: {
                enabled: true,
                stabilization: {
                    iterations: 200
                },
                barnesHut: {
                    gravitationalConstant: -26000,
                    centralGravity: 0.3,
                    springLength: 300,
                    springConstant: 0.04,
                    damping: 0.3
                }
            },
            nodes: {
                font: {
                    size: 14,
                    color: 'black'
                },
                borderWidth: 2,
                shadow: true
            },
            edges: {
                arrows: 'to',
                font: {
                    size: 12,
                    align: 'middle'
                },
                shadow: true,
                smooth: {
                    type: 'continuous'
                }
            },
            interaction: {
                navigationButtons: true,
                keyboard: true,
                zoomView: true,
                dragView: true
            }
        };

        var network = new vis.Network(container, data, options);

        function resetView() {
            network.fit();
        }

        function fitToScreen() {
            network.fit({
                animation: {
                    duration: 1000,
                    easingFunction: 'easeInOutQuad'
                }
            });
        }

        window.addEventListener('resize', function() {
            network.redraw();
        });

        network.fit();
    </script>
</body>
</html>
"@

    # HTMLファイルを保存
    $htmlContent | Set-Content $OutputPath -Encoding UTF8

    Write-Host "ネットワークルーティンググラフをHTMLで生成しました: $OutputPath"
    Write-Host "ブラウザで表示しています..."

    # ブラウザで開く
    Invoke-Item $OutputPath
}

# 関数を実行(スクリプトの引数を渡す)
$params = @{
    OutputPath = $OutputPath
}
if ($IncludeDefaultRoutes) {
    $params['IncludeDefaultRoutes'] = $IncludeDefaultRoutes
}
if ($ExcludeNextHops.Count -gt 0) {
    $params['ExcludeNextHops'] = $ExcludeNextHops
}
Visualize-NetRouteGraph @params

最後に

作成された HTML をブラウザ開けば OK です

2026年2月11日水曜日

GAE 用のバケットやサービスアカウントを削除して deploy できなくなった場合の対処方法

GAE 用のバケットやサービスアカウントを削除して deploy できなくなった場合の対処方法

概要

間違って GAE 用のバケットやサービスアカウントを削除してしまった素直に repair しましょう

環境

  • gcloud 545.0.0

コマンド

  • gcloud beta app repair

最後に

ドメイン入りのバケットは手動では作成できません

参考サイト

2026年2月10日火曜日

Outlook の受信トレイにあるメールを検索する Powershell

Outlook の受信トレイにあるメールを検索する Powershell

概要

タイトル通り

環境

  • Windows 11

スクリプト

param(
    [string]$Keyword = "test",
    [switch]$SearchBody = $false
)

try {
    Write-Host "Connecting to Outlook..."

    $ErrorActionPreference = "Stop"

    $Outlook = New-Object -ComObject Outlook.Application
    $Namespace = $Outlook.GetNamespace("MAPI")
    $Namespace.Logon()

    $Inbox = $Namespace.GetDefaultFolder(6)

    Write-Host "Searching in $($Inbox.Name)"
    Write-Host "Keyword: $Keyword"
    Write-Host "Search Body: $SearchBody"

    $Items = $Inbox.Items
    Write-Host "Total items: $($Items.Count)"

    $FoundItems = @()
    $count = 0
    foreach ($Item in $Items) {
        $count++
        if ($count % 100 -eq 0) {
            Write-Host "Processing item $count..."
        }
        if ($SearchBody) {
            if ($Item.Subject -match $Keyword -or $Item.Body -match $Keyword) {
                $FoundItems += $Item
            }
        }
        else {
            if ($Item.Subject -match $Keyword) {
                $FoundItems += $Item
            }
        }
    }

    Write-Host "Found: $($FoundItems.Count) emails"

    if ($FoundItems.Count -gt 0) {
        foreach ($MailItem in $FoundItems) {
            Write-Host "-----"
            Write-Host "Subject: $($MailItem.Subject)"
            Write-Host "Sender: $($MailItem.SenderName)"
            Write-Host "Received: $($MailItem.ReceivedTime)"
        }
    }
    else {
        Write-Host "No emails found."
    }

    if ($Inbox) { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($Inbox) | Out-Null }
    if ($Namespace) { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($Namespace) | Out-Null }
    if ($Outlook) { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($Outlook) | Out-Null }

    Write-Host "Done."
}
catch {
    Write-Host "Error: $($_.Exception.Message)"
}

最後に

日本語で検索すると文字コードエラーになる場合があるのでその場合は検索キーワードを引数で受け取るようにしましょう
メールの件数が多いとかなり時間がかかるのでタイトルだけで検索するなどの工夫をしましょう
進捗がわかりづらい場合などがあるので 100件ずつ分割して検索してもいいと思います

2026年2月9日月曜日

外部の Redis にアクセスする場合はちゃんと pipeline を使おう

外部の Redis にアクセスする場合はちゃんと pipeline を使おう

概要

ループごとに GET するような処理はネットワークの負荷が重くなり結果としてアプリのレスポンス遅延にもつながるので可能な限り1回でほしい結果はすべて取得するようにしましょう

環境

  • Python 3.12.11
  • redis-py 7.1.0
  • redis 8.4.0

ダメなコード

def get_index_pairs(redis_client, hand_images: dict, ai_images: dict) -> list:
    profiles = get_profiles()
    pairs = []
    for num in sorted(
        set(hand_images.keys()) | set(ai_images.keys()),
        key=lambda x: int(x),
        reverse=True,
    ):
        hand_like_key = f"{num}_hand"
        ai_like_key = f"{num}_ai"
        hand_like = int(redis_client.get(hand_like_key) or 0)  # type: ignore
        ai_like = int(redis_client.get(ai_like_key) or 0)  # type: ignore
        pair = {
            "num": num,
            "hand": hand_images.get(num),
            "ai": ai_images.get(num),
            "hand_like": hand_like,
            "ai_like": ai_like,
            "name_hand": profiles.get(hand_like_key, {}).get("name", "名無し"),
            "character_hand": profiles.get(hand_like_key, {}).get(
                "character", "考え中"
            ),
            "name_ai": profiles.get(ai_like_key, {}).get("name", "名無し"),
            "character_ai": profiles.get(ai_like_key, {}).get("character", "考え中"),
            "winner": (
                "hand" if hand_like > ai_like else "ai" if ai_like > hand_like else None
            ),
        }
        pairs.append(pair)
    return pairs

良いコード

def get_index_pairs(redis_client, hand_images: dict, ai_images: dict) -> list:
    profiles = get_profiles()
    sorted_nums = sorted(
        set(hand_images.keys()) | set(ai_images.keys()),
        key=lambda x: int(x),
        reverse=True,
    )

    # --- Pipeline の開始 ---
    pipe = redis_client.pipeline()
    for num in sorted_nums:
        pipe.get(f"{num}_hand")
        pipe.get(f"{num}_ai")

    # 1回の通信で全データを取得
    raw_results = pipe.execute()
    # --- Pipeline の終了 ---

    pairs = []
    # 取得した結果を 2 つずつ取り出す
    for i, num in enumerate(sorted_nums):
        hand_like_bytes = raw_results[i * 2]
        ai_like_bytes = raw_results[i * 2 + 1]

        # decode_responses=False の場合を考慮
        hand_like = int(hand_like_bytes.decode() if hand_like_bytes else 0)
        ai_like = int(ai_like_bytes.decode() if ai_like_bytes else 0)

        pair = {
            "num": num,
            "hand": hand_images.get(num),
            "ai": ai_images.get(num),
            "hand_like": hand_like,
            "ai_like": ai_like,
            "name_hand": profiles.get(f"{num}_hand", {}).get("name", "名無し"),
            "character_hand": profiles.get(f"{num}_hand", {}).get(
                "character", "考え中"
            ),
            "name_ai": profiles.get(f"{num}_ai", {}).get("name", "名無し"),
            "character_ai": profiles.get(f"{num}_ai", {}).get("character", "考え中"),
            "winner": (
                "hand" if hand_like > ai_like else "ai" if ai_like > hand_like else None
            ),
        }
        pairs.append(pair)
    return pairs

比較

ネットワーク・レイテンシの削減

これが最も大きな改善点です。

ダメなコード(逐次処理): ループの中で毎回 redis_client.get() を呼び出しています。もし num が100個あれば、200回の通信(リクエストとレスポンスの往復)が発生します。

良いコード(パイプライン): すべての get コマンドを一旦溜めてから、pipe.execute() で 1回だけ Redisサーバーに送ります。通信回数が1回(往復1回)で済むため、ネットワークの遅延(レイテンシ)の影響を最小限に抑えられます。

スループット(処理能力)の向上

Redisサーバー側の負荷も軽減されます。

ダメなコード: サーバーは「リクエスト受信 → 解析 → 実行 → レスポンス送信」というサイクルを200回繰り返します。

良いコード: サーバーは「大きなリクエストを1つ受信 → まとめて実行 → 大きなレスポンスを1つ送信」という挙動になります。これにより、コンテキストスイッチやパケット処理のオーバーヘッドが減り、全体のスループットが向上します。

型の安全性と堅牢な実装

細かい部分ですが、データの扱いも丁寧になっています。

デコード処理: hand_like_bytes.decode() if hand_like_bytes else 0 のように、Redisから返ってくる値が None(キーが存在しない)場合や、バイト列(bytes型)である可能性を考慮して明示的にデコードしています。

マジックナンバーの排除: i * 2 と i * 2 + 1 を使ってパイプラインの結果から正しくデータを取り出しており、インデックス管理が明確です。

最後に

開発初期ではデータが少なく問題にならないですがデータが増えてくるとアプリの挙動がおかしくなるので注意しましょう

gunicorn などを使っていると Redis の応答よりもワーカーが先に死亡し以下のようなエラーになります

Traceback (most recent call last):
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/gunicorn/workers/sync.py", line 142, in handle
    self.handle_request(listener, req, client, addr)
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/gunicorn/workers/sync.py", line 185, in handle_request
    respiter = self.wsgi(environ, resp.start_response)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/flask/app.py", line 1536, in __call__
    return self.wsgi_app(environ, start_response)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/werkzeug/middleware/proxy_fix.py", line 183, in __call__
    return self.app(environ, start_response)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/flask/app.py", line 1511, in wsgi_app
    response = self.full_dispatch_request()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/flask/app.py", line 917, in full_dispatch_request
    rv = self.dispatch_request()
         ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/flask/app.py", line 902, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)  # type: ignore[no-any-return]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/data/repo/ai-gallery/app.py", line 162, in index
    pairs = get_pairs()
            ^^^^^^^^^^^
  File "/Users/username/data/repo/ai-gallery/app.py", line 143, in get_pairs
    pairs = get_index_pairs(redis_client, hand_images, ai_images)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/data/repo/ai-gallery/lib/pair.py", line 45, in get_index_pairs
    ai_like = int(redis_client.get(ai_like_key) or 0)  # type: ignore
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/commands/core.py", line 1923, in get
    return self.execute_command("GET", name, keys=[name])
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/client.py", line 657, in execute_command
    return self._execute_command(*args, **options)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/client.py", line 668, in _execute_command
    return conn.retry.call_with_retry(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/retry.py", line 116, in call_with_retry
    return do()
           ^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/client.py", line 669, in <lambda>
    lambda: self._send_command_parse_response(
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/client.py", line 640, in _send_command_parse_response
    return self.parse_response(conn, command_name, **options)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/client.py", line 691, in parse_response
    response = connection.read_response()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/connection.py", line 1133, in read_response
    response = self._parser.read_response(disable_decoding=disable_decoding)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/_parsers/resp2.py", line 15, in read_response
    result = self._read_response(disable_decoding=disable_decoding)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/_parsers/resp2.py", line 25, in _read_response
    raw = self._buffer.readline()
          ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/_parsers/socket.py", line 115, in readline
    self._read_from_socket()
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/redis/_parsers/socket.py", line 65, in _read_from_socket
    data = self._sock.recv(socket_read_size)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/username/.local/share/virtualenvs/ai-gallery-nT0ArqSL/lib/python3.12/site-packages/gunicorn/workers/base.py", line 198, in handle_abort
    sys.exit(1)
SystemExit: 1

2026年2月8日日曜日

VSCode で Powershell をフォーマットする方法

VSCode で Powershell をフォーマットする方法

概要

VSCode を使うのが一番簡単のようです

環境

  • Windows11
  • VSCode 1.108.2

フォーマット

  • ps1 ファイルを開く
  • 右クリックから「Format Document」

リント

lint は CLI でできます

Invoke-ScriptAnalyzer -Path .\test.ps1

最後に

全部 CLI でやりたい

参考サイト

2026年2月7日土曜日

Slack で管理者権限のあるユーザトークンを取得する方法

Slack で管理者権限のあるユーザトークンを取得する方法

概要

ボットトークン (xoxb-) ではなくユーザトークン (xoxp-) を取得する方法を紹介します
ただし無料プランだと管理者権限のあるユーザトークンは設定できないので注意してください

環境

  • Slack 2026/02/04 時点

アプリ作成

権限設定

  • OAuth & Permissions
  • User Token Scopes
  • admin を追加

アプリ再インストール

  • OAuth & Permissions
  • OAuth Tokens
  • Reinstall to xxx

トークン確認

  • xoxp- から始まるトークンが発行されていることを確認します
  • すでにボットトークンがある場合はその上に User OAuth Token が発行されます

最後に

あとはトークンを使うだけ

参考サイト

2026年2月6日金曜日

GCP のクラウドストレージに上書きアップロードしてもオブジェクトが変更されない場合の対応方法

GCP のクラウドストレージに上書きアップロードしてもオブジェクトが変更されない場合の対応方法

概要

おそらくキャッシュされているのが問題です
特に発生しやすいのが「同じような画像を上書きアップロードした場合」で例えば元の画像を90度回転させた画像などは上書きアップロードしても反映されないことが多いです

そんな場合の反映方法を紹介します

環境

  • GCP 2026/02/04 時点

ブラウザのキャッシュを削除する

とりあえず削除して再度アクセスしてみましょう

クエリ付きでアクセスする

クラウドストレージ側でキャッシュしている可能性があるのでクエリ付きでアクセスしましょう

https://storage.googleapis.com/your_bucket_name/file.jpg?nocache=1

Cache-Control を確認し短くする

curl -I https://storage.googleapis.com/your_bucket_name/file.jpg

これで「Cache-Control:」「Age:」「X-Cache:」があればクラウドストレージ側のキャッシュが有効になっています

  • gsutil setmeta -h "Cache-Control:public,max-age=60" gs://your_bucket_name/file.jpg

60秒など短い時間にしましょう
0 だと毎回クラウドストレージにアクセスしネットワーク量が多くなり金額上がるので注意しましょう

別のファイル名にする

これが一番カンタンです
がアプリ側の改修などが必要になるケースがあるので注意しましょう

よくあるパターンはファイル名にハッシュなどを含めるケースです

ダメだったケース

  • 削除 -> 再度アップロード
    • なぜか元の画像がクラウドストレージ側でも利用されてしまう
  • 実はファイルが回転できていない
    • exif の Orientation にまだデータが残っている
    • ビューアの見た目だけが回転している

最後に

まずはキャッシュを疑いましょう