Android 大文件上传失败问题排查与解决

December 20, 2025
4 min read
By devshan

Table of Contents

This is a list of all the sections in this post. Click on any of them to jump to that section.

问题描述

在 Android 设备上批量上传视频文件时,部分文件上传失败。

现象

  • 选择 6 个视频文件(20MB ~ 60MB)上传
  • 只有 2 个成功,4 个失败
  • 前端显示 Broken pipestatus code 400 错误
  • 服务器日志显示 permission denied 错误

问题定位过程

第一步:添加调试日志

为了定位问题,在前后端添加详细的上传日志:

前端日志(api_client.dart):

Future<ApiResult<FileMetadata>> uploadFile(...) async {
  print('[UPLOAD] 开始上传: $fileName, 路径: $filePath');
  print('[UPLOAD] FormData 准备完成, 开始发送请求...');
  // ...
  print('[UPLOAD] 数据发送完成: $fileName, 等待服务器响应...');
  print('[UPLOAD] 服务器响应: ${response.statusCode}');
}

后端日志(server.go):

func (s *Server) uploadFile(c *gin.Context) {
    log.Printf("[UPLOAD] 收到上传请求")
    log.Printf("[UPLOAD] 文件名: %s, 大小: %d bytes", header.Filename, header.Size)
    log.Printf("[UPLOAD] 开始加密: %s", header.Filename)
    log.Printf("[UPLOAD] 加密完成, 加密后大小: %d bytes", encryptedBuf.Len())
    log.Printf("[UPLOAD] 开始上传到 S3: %s", s3Key)
    // ...
}

第二步:复现问题

运行应用,选择 6 个视频文件上传,观察日志:

I/flutter: [UPLOAD] 开始上传: VID_20251212_185648.mp4
I/flutter: [UPLOAD] 开始上传: VID_20251212_185621.mp4
I/flutter: [UPLOAD] 开始上传: VID_20251212_185550.mp4
I/flutter: [UPLOAD] FormData 准备完成, 开始发送请求...
I/flutter: [UPLOAD] FormData 准备完成, 开始发送请求...
I/flutter: [UPLOAD] FormData 准备完成, 开始发送请求...
I/GoLog: [UPLOAD] 收到上传请求
I/GoLog: [UPLOAD] 收到上传请求
I/GoLog: [UPLOAD] 收到上传请求
I/GoLog: [UPLOAD] 文件名: VID_20251212_185550.mp4, 大小: 27809735 bytes
I/GoLog: [UPLOAD] 开始加密: VID_20251212_185550.mp4
I/GoLog: [UPLOAD] 加密完成, 加密后大小: 27810729 bytes
I/GoLog: [UPLOAD] 开始上传到 S3: files/xxx.enc
I/GoLog: [UPLOAD] 无文件: open /data/local/tmp/multipart-2734234005: permission denied ← 关键错误
I/GoLog: [UPLOAD] 无文件: open /data/local/tmp/multipart-2852574493: permission denied ← 关键错误
I/flutter: [UPLOAD] 上传失败: VID_20251212_185648.mp4, 错误: Broken pipe
I/flutter: [UPLOAD] 上传失败: VID_20251212_185621.mp4, 错误: Broken pipe

第三步:分析错误原因

关键错误信息:

open /data/local/tmp/multipart-2734234005: permission denied

根本原因分析

  1. Gin 框架的 multipart 处理机制

    • Gin 默认 MaxMultipartMemory = 32MB
    • 当上传文件大小超过此限制时,Gin 会将数据写入临时文件
    • 默认临时目录由 Go 的 os.TempDir() 决定
  2. Android 的临时目录问题

    • Go 在 Android 上默认使用 /data/local/tmp/ 作为临时目录
    • 但 Android 应用没有此目录的读写权限
    • 只有 shell/adb 进程才能访问此目录
  3. 并发上传放大问题

    • 同时上传 3 个大文件
    • 第一个文件可能恰好小于等于内存限制,成功
    • 后续文件超限后写入临时目录,失败
┌─────────────────────────────────────────────────────────────┐
│ Gin Multipart 处理流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 上传请求 ──► 检查文件大小 │
│ │ │
│ ┌───────┴───────┐ │
│ ▼ ▼ │
│ ≤ MaxMultipartMemory > MaxMultipartMemory │
│ │ │ │
│ ▼ ▼ │
│ 存储在内存 写入临时文件 │
│ │ │ │
│ │ ▼ │
│ │ /data/local/tmp/multipart-xxx │
│ │ │ │
│ │ ▼ │
│ │ ❌ permission denied │
│ │ │ │
│ ▼ ▼ │
│ 成功 ✓ 失败 ✗ │
│ │
└─────────────────────────────────────────────────────────────┘

解决方案演进

方案一:增大内存限制(不推荐)

最初的想法是增大 MaxMultipartMemory,让所有文件都在内存中处理:

func NewServer(cfg *config.Config) (*Server, error) {
    router := gin.New()
    router.MaxMultipartMemory = 512 << 20 // 512 MB
    // ...
}

问题

  • 移动设备内存有限,512MB 可能导致 OOM
  • 并发上传多个大文件时仍可能超限
  • 治标不治本

方案二:设置正确的临时目录(推荐)

真正的解决方案是让 Go 使用应用有权限的临时目录。

分析

  • Flutter 启动核心时传入了 LogDir 参数
  • 这个目录是 /data/user/0/com.e2eepan.e2eepan_client/app_flutter/
  • 应用对此目录有完全的读写权限

修改 mobile.go

func Start(cfg *CoreConfig) error {
    mu.Lock()
    defer mu.Unlock()
 
    if httpSrv != nil {
        return nil
    }
 
    // 设置日志和临时目录
    if cfg != nil && cfg.LogDir != "" {
        setupLogFile(cfg.LogDir)
        
        // 关键修复:设置临时目录为应用有权限的目录
        tmpDir := filepath.Join(cfg.LogDir, "tmp")
        if err := os.MkdirAll(tmpDir, 0755); err == nil {
            os.Setenv("TMPDIR", tmpDir)
        }
    }
 
    // ... 后续初始化
}

修改 server.go(移除之前的内存限制):

func NewServer(cfg *config.Config) (*Server, error) {
    gin.SetMode(gin.ReleaseMode)
    router := gin.New()
    router.Use(gin.Recovery())
 
    // 使用默认 MaxMultipartMemory (32MB)
    // 超过此限制的文件会写入临时目录(已在 mobile.go 中设置为应用有权限的目录)
 
    server := &Server{
        // ...
    }
}

验证修复

重新编译运行后,上传 5 个视频文件(20MB ~ 60MB)全部成功:

I/flutter: [UPLOAD] 开始上传: VID_20251212_185648.mp4
I/flutter: [UPLOAD] 开始上传: VID_20251212_185621.mp4
I/flutter: [UPLOAD] 开始上传: VID_20251212_185550.mp4
I/GoLog: [UPLOAD] 收到上传请求
I/GoLog: [UPLOAD] 收到上传请求
I/GoLog: [UPLOAD] 收到上传请求
I/GoLog: [UPLOAD] 文件名: VID_20251212_185550.mp4, 大小: 27809735 bytes
I/GoLog: [UPLOAD] S3 上传成功: files/xxx.enc
I/GoLog: [UPLOAD] 文件名: VID_20251212_185648.mp4, 大小: 60129443 bytes
I/GoLog: [UPLOAD] S3 上传成功: files/xxx.enc
I/GoLog: [UPLOAD] 文件名: VID_20251212_185621.mp4, 大小: 62375956 bytes
I/GoLog: [UPLOAD] S3 上传成功: files/xxx.enc
I/GoLog: [UPLOAD] 文件名: VID_20251212_185343.mp4, 大小: 20398691 bytes
I/GoLog: [UPLOAD] S3 上传成功: files/xxx.enc
I/GoLog: [UPLOAD] 文件名: VID_20251212_185245.mp4, 大小: 35762449 bytes
I/GoLog: [UPLOAD] S3 上传成功: files/xxx.enc

所有 5 个视频文件都成功上传到 S3 并出现在文件列表中。

技术要点总结

Go 临时目录机制

Go 的 os.TempDir() 函数按以下优先级选择临时目录:

  1. $TMPDIR 环境变量(如果设置)
  2. /tmp(Unix 系统)
  3. C:\Users\xxx\AppData\Local\Temp(Windows)

在 Android 上,由于应用沙盒机制,只有以下目录可写:

  • /data/user/0/<package>/ - 应用私有数据目录
  • /data/user/0/<package>/cache/ - 应用缓存目录
  • /sdcard/Android/data/<package>/ - 外部存储(需要权限)

Gin Multipart 处理

Gin 框架使用 Go 标准库的 multipart 包处理文件上传:

// 默认值
const defaultMaxMultipartMemory = 32 << 20 // 32 MB
 
// Engine 结构
type Engine struct {
    MaxMultipartMemory int64
    // ...
}
 
// 处理 multipart 请求
func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
    if c.Request.MultipartForm == nil {
        // 解析 multipart,超过 MaxMultipartMemory 的部分写入临时文件
        if err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil {
            return nil, err
        }
    }
    // ...
}

gomobile 编译缓存

在调试过程中遇到一个坑:修改 Go 代码后,运行脚本没有生效。

原因是 gomobile bind 可能使用了缓存。解决方法是删除旧的 .aar 文件:

REM run_android.bat
@echo off
setlocal
 
REM Delete old .aar to force recompile
if exist "%~dp0..\..\client\android\app\libs\e2eepan-mobile.aar" (
  del /q "%~dp0..\..\client\android\app\libs\e2eepan-mobile.aar"
)
 
pushd "%~dp0..\..\core"
gomobile bind -target=android -androidapi=21 -o ..\client\android\app\libs\e2eepan-mobile.aar e2eepan/mobile

经验教训

  1. 不要无脑加大内存限制

    • 这只是治标不治本
    • 移动设备内存有限,可能导致 OOM
    • 更好的做法是找到根本原因
  2. 理解平台差异

    • Go 代码在不同平台上的行为可能不同
    • Android 的沙盒机制限制了文件系统访问
    • 需要显式配置才能让代码在移动端正常工作
  3. 添加足够的日志

    • 调试分布式/跨平台系统时,日志是最重要的工具
    • 在关键路径添加日志可以快速定位问题
    • 问题解决后记得清理调试日志
  4. gomobile 编译缓存

    • 修改 Go 代码后如果没生效,检查是否使用了缓存
    • 可以通过删除 .aar 文件强制重新编译

相关文件

  • core/mobile/mobile.go - 设置 TMPDIR 环境变量
  • core/internal/api/server.go - 上传处理逻辑
  • client/lib/core/api/api_client.dart - 前端上传逻辑
  • scripts/bat/run_android.bat - Android 编译运行脚本

参考