从Azure下载Blob并解压的Go代码问题排查
我有一段从Azure Storage下载Blob并解压的Go代码,但解压函数抛出错误:bytes.Reader.ReadAt: negative offsetBlob decompressed contents。传入readBlob函数的参数是正确的,代码能打印出Zip文件内的文件名,但无法打开Zip中的文件。直接通过Azure Storage Explorer下载该文件,用Windows资源管理器可正常打开Zip及其中内容。
代码如下:
func readBlob(client *azblob.Client, ctx context.Context, containerName string, blobName string) { // download the blob // will want to do this in the worker pool and not in the main loader thread get, err := client.DownloadStream(ctx, containerName, blobName, nil) handleError(err) downloadedData := bytes.Buffer{} retryReader := get.NewRetryReader(ctx, &azblob.RetryReaderOptions{}) _, err = downloadedData.ReadFrom(retryReader) handleError(err) err = retryReader.Close() handleError(err) file, err := decompress(blobName, buff) if err != nil { fmt.Println("Error decompressing blob: ", blobName) fmt.Print(err) } } func decompress(blobName string, downloadedData bytes.Buffer) (string, error) { // try to decompress as zip zipReader, err := zip.NewReader(bytes.NewReader(downloadedData.Bytes()), int64(len(downloadedData.Bytes()))) if err != nil { return "", err } for _, f := range zipReader.File { // the next line can add uncomment for troubleshooting and DOES print the right file name - does not necessary mean the blob download or the synnex of NewReader is perfect - but does validate it somewhat // fmt.Println("ffile name in zip file: ", f.Name) rc, err := f.Open() // THIS IS where the error is returned if err != nil { return "", err } defer rc.Close() decompressedData, err := io.ReadAll(rc) if err != nil { return "", err } return string(decompressedData), nil // only care about the first file in the zip file } }
1. 变量传递笔误
readBlob函数最后调用decompress时传入的是buff,但实际存储Blob数据的变量是downloadedData。这个错误会导致解压函数拿到空数据或错误数据,直接引发后续解压异常,修正为传入downloadedData即可。
2. ZipReader大小参数不准确
zip.NewReader的第二个参数需要准确传入Zip文件的总字节数。当前代码用int64(len(downloadedData.Bytes()))做类型转换,虽然逻辑上可行,但更稳妥的方式是直接使用downloadedData.Size()(bytes.Buffer的Size()方法直接返回int64类型的内容长度,避免转换误差)。如果大小参数与实际数据长度不匹配,Zip内部的偏移计算会出错,导致ReadAt出现负偏移错误。
3. Blob数据未完整下载
虽然能看到Zip内的文件名,但可能只下载了Zip的目录部分,文件内容未完整下载。可以添加下载完整性校验:
- 先获取Blob属性拿到预期大小
- 下载后对比实际读取的字节数与预期大小是否一致
修正后的代码示例
func readBlob(client *azblob.Client, ctx context.Context, containerName string, blobName string) { // 获取Blob属性,确认预期大小 props, err := client.GetBlobProperties(ctx, containerName, blobName, nil) handleError(err) expectedSize := props.ContentLength // 下载Blob get, err := client.DownloadStream(ctx, containerName, blobName, nil) handleError(err) downloadedData := bytes.Buffer{} retryReader := get.NewRetryReader(ctx, &azblob.RetryReaderOptions{}) readBytes, err := downloadedData.ReadFrom(retryReader) handleError(err) // 校验下载完整性 if readBytes != expectedSize { fmt.Printf("下载不完整:预期%d字节,实际%d字节\n", expectedSize, readBytes) return } err = retryReader.Close() handleError(err) // 修正变量传递错误 file, err := decompress(blobName, downloadedData) if err != nil { fmt.Println("Error decompressing blob: ", blobName) fmt.Print(err) } } func decompress(blobName string, downloadedData bytes.Buffer) (string, error) { // 使用准确的文件大小初始化ZipReader zipReader, err := zip.NewReader(bytes.NewReader(downloadedData.Bytes()), downloadedData.Size()) if err != nil { return "", err } for _, f := range zipReader.File { rc, err := f.Open() if err != nil { return "", err } defer rc.Close() decompressedData, err := io.ReadAll(rc) if err != nil { return "", err } return string(decompressedData), nil } return "", fmt.Errorf("zip文件中无有效内容") }
额外排查步骤
如果问题仍存在,可将下载的downloadedData写入本地文件,检查该文件是否能正常解压,以此确认下载的数据是否完整、未被篡改。
内容的提问来源于stack exchange,提问作者inf

