You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用PowerShell迁移Azure WebApp日志至存储账户遇问题求助

Azure WebApp日志备份至存储账户并删除源文件的问题解决

问题概述

尝试通过PowerShell将Azure WebApp的日志文件复制到Azure存储账户容器,完成后删除源日志,但遇到以下两个问题:

  1. 复制到Blob时出现警告,无文件被复制:

WARNING: Ignore mismatch source storage context.. The source uri is https://wa01.scm.azurewebsites.net/api/vfs/LogFiles/kudu/, the end point is https://sa01.blob.core.windows.net/.

  1. 删除操作报错:

Invoke-RestMethod : {"Message":"Cannot delete directory. It is either not empty or access is not allowed."}

需求:将WebApp中的文件和目录完整复制到存储容器,完成后删除源文件和目录。


问题1:复制Blob失败的解决方案

Start-AzStorageBlobCopy仅支持Azure存储服务之间的直接复制,或带有SAS授权的外部URL,无法直接从Kudu的HTTP API URL复制文件。正确做法是先通过Kudu API下载日志文件到本地临时目录,再上传至存储账户。同时需补充缺失的$cutoffDate定义和存储账户上下文获取逻辑。

问题2:删除目录失败的解决方案

Kudu API无法直接删除非空目录,必须先递归删除目录内的所有文件,再删除空目录。需修改删除逻辑,先处理文件,再从最深层开始删除空目录。


修正后的完整代码

$webAppName  = "WA01"
$resourceGroupName = "RG01"
$storageAccountName = "SA01"
$containerName = "container01"
$numberOfDays = 90
$tempDir = Join-Path $env:TEMP "WebAppLogsBackup"

# 创建临时目录
if (-not (Test-Path $tempDir)) {
    New-Item -ItemType Directory -Path $tempDir | Out-Null
}

# 计算截止日期
$cutoffDate = (Get-Date).AddDays(-$numberOfDays)

# 获取存储账户上下文
$storageAccountContext = (Get-AzStorageAccount -ResourceGroupName $resourceGroupName -Name $storageAccountName).Context

# Generate Kudu API URL for log files
$kuduApiUrl = "https://${webAppName}.scm.azurewebsites.net/api/vfs/LogFiles/"

# Retrieve publishing profile for the Azure Web App
Write-Host "Retrieving publishing profile for $webAppName..."
$publishingProfile = Get-AzWebAppPublishingProfile -ResourceGroupName $resourceGroupName -Name $webAppName

# Parse publishing profile XML to get credentials
[xml]$publishingProfileXml = $publishingProfile
$publishingUserName = $publishingProfileXml.publishData.publishProfile[0].userName
$publishingPassword = $publishingProfileXml.publishData.publishProfile[0].userPWD

# Convert credentials to base64
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("${publishingUserName}:${publishingPassword}"))
$headers = @{Authorization=("Basic $base64AuthInfo")}

# 递归获取所有符合条件的文件和目录
function Get-KuduItems {
    param(
        [string]$uri,
        [DateTime]$cutoffDate
    )
    $items = Invoke-RestMethod -Uri $uri -Headers $headers -Method GET
    $result = @()
    foreach ($item in $items) {
        if ($item.type -eq "file") {
            $mtimeValue = $item.mtime.Split('.')[0].Substring(0, 19)
            $fileDate = [DateTime]::ParseExact($mtimeValue, "yyyy-MM-dd'T'HH:mm:ss", [System.Globalization.CultureInfo]::InvariantCulture)
            if ($fileDate -lt $cutoffDate) {
                $result += $item
            }
        } elseif ($item.type -eq "directory") {
            # 递归获取子目录中的文件
            $result += Get-KuduItems -uri $item.href -cutoffDate $cutoffDate
            # 记录目录本身,后续删除
            $result += $item
        }
    }
    return $result
}

Write-Host "Retrieving log files and directories from $webAppName..."
$allItems = Get-KuduItems -uri $kuduApiUrl -cutoffDate $cutoffDate

# 分离文件和目录(文件先处理,目录最后删除,按路径长度倒序确保从最深目录开始删)
$filesToBackup = $allItems.Where({$_.type -eq "file"})
$dirsToDelete = $allItems.Where({$_.type -eq "directory"}) | Sort-Object {$_.href.Length} -Descending

# Perform backup actions...
Write-Host "Performing backup actions for $webAppName..."
foreach ($file in $filesToBackup) {
    $logFileName = $file.name
    $logFileUrl = $file.href
    $localFilePath = Join-Path $tempDir $logFileName
    
    Write-Host "Downloading log file: $logFileName"
    Invoke-RestMethod -Uri $logFileUrl -Headers $headers -Method GET -OutFile $localFilePath
    
    # 构造Blob路径,保留原目录结构
    $relativePath = $logFileUrl.Replace($kuduApiUrl, "")
    $blobName = "$webAppName/$relativePath"
    
    Write-Host "Uploading to blob: $blobName"
    Set-AzStorageBlobContent -File $localFilePath -Container $containerName -Blob $blobName -Context $storageAccountContext -Force | Out-Null
    
    # 删除临时文件
    Remove-Item $localFilePath -Force | Out-Null
}

# Perform deletion actions...
Write-Host "Deleting log files older than $numberOfDays days from $webAppName..."
foreach ($file in $filesToBackup) {
    $logFileName = $file.name
    Write-Host "Deleting log file: $logFileName"
    Invoke-RestMethod -Uri $file.href -Headers $headers -Method DELETE | Out-Null
}

Write-Host "Deleting empty directories from $webAppName..."
foreach ($dir in $dirsToDelete) {
    $dirName = $dir.name
    Write-Host "Deleting directory: $dirName"
    try {
        Invoke-RestMethod -Uri $dir.href -Headers $headers -Method DELETE | Out-Null
    } catch {
        Write-Warning "Failed to delete directory $dirName (may still contain files)"
    }
}

# 清理临时目录
Remove-Item $tempDir -Recurse -Force | Out-Null

关键修改说明

  • 补充$cutoffDate定义,计算90天前的日期,实现按时间筛选日志
  • 添加存储账户上下文$storageAccountContext的获取逻辑,确保上传Blob时权限正常
  • 新增递归函数Get-KuduItems,遍历WebApp日志目录下所有文件和子目录,筛选符合时间条件的项
  • 替换Start-AzStorageBlobCopy为本地下载+Blob上传的流程,解决Kudu URL无法直接复制的问题
  • 调整删除顺序:先删除所有符合条件的文件,再从最深层目录开始删除空目录,避免非空目录删除报错
  • 添加临时目录的创建与清理逻辑,避免本地文件残留

内容的提问来源于stack exchange,提问作者SKumar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 06:55:21