Golang文件上传至Google Cloud Storage失败问题求助

Golang文件上传至Google Cloud Storage失败问题求助 各位Gopher们好,

我正在构建一个小型个人应用,它接收分块文件然后上传到Google Cloud Blob Storage。 不幸的是,文件似乎从未成功上传到Google Cloud(看起来是这样),那里没有任何显示。也没有任何错误。

有人发现这个应用有什么问题吗? 感谢任何想法。

func main() {
	router := gin.Default()

	config := cors.DefaultConfig()
	config.AllowAllOrigins = true
	config.AllowHeaders = []string{"Content-Type", "Content-Length", "Content-Range", "Accept-Encoding", "X-CSRF-Token", "Authorization", "accept", "origin", "Cache-Control", "X-Requested-With"}
	router.Use(cors.New(config))

	rg := router.Group("api/v1")
	{
		rg.POST("/photo", uploadFile)
	}

	router.Run()
}

func uploadFile(c *gin.Context) {
	var f *os.File
	file, header, e := c.Request.FormFile("file")

	if f == nil {
		f, e = os.OpenFile(header.Filename, os.O_APPEND|os.O_CREATE|os.O_RDWR, os.ModeAppend)
		if e != nil {
			panic("Error creating file on the filesystem: " + e.Error())
		}
		defer f.Close()
	}

	if _, e := io.Copy(f, file); e != nil {
		panic("Error during chunk write:" + e.Error())
	}

	if isFileUploadCompleted(c) {
		uploadToGoogle(c, f)
	}
}

func isFileUploadCompleted(c *gin.Context) bool {
	contentRangeHeader := c.Request.Header.Get("Content-Range")
	rangeAndSize := strings.Split(contentRangeHeader, "/")
	rangeParts := strings.Split(rangeAndSize[0], "-")

	rangeMax, e := strconv.Atoi(rangeParts[1])
	if e != nil {
		panic("Could not parse range max from header")
	}

	fileSize, e := strconv.Atoi(rangeAndSize[1])
	if e != nil {
		panic("Could not parse file size from header")
	}

	return fileSize == rangeMax
}

func uploadToGoogle(c *gin.Context, f *os.File) {
	creds, isFound := os.LookupEnv("GOOGLE_APPLICATION_CREDENTIALS")
	if !isFound {
		panic("GCP environment variable is not set")
	} else if creds == "" {
		panic("GCP environment variable is empty")
	}
	const bucketName = "el-my-gallery"

	client, e := storage.NewClient(c)
	if e != nil {
		panic("Error creating client: " + e.Error())
	}
	defer client.Close()

	bucket := client.Bucket(bucketName)
	w := bucket.Object(f.Name()).NewWriter(c)
	fmt.Printf("%v is the upload filename", f.Name())
	fmt.Println()

	f.Seek(0, io.SeekStart)
	if bw, e := io.Copy(w, f); e != nil {
		panic("Error during GCP upload:" + e.Error())
	} else {
		fmt.Printf("%v bytes written to Cloud", bw)
		fmt.Println()
	}
}

对于这行代码 fmt.Printf("%v bytes written to Cloud", bw),我确实得到了很多已写入的字节数。


更多关于Golang文件上传至Google Cloud Storage失败问题求助的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

修复很简单,写入器没有关闭。 表情

更多关于Golang文件上传至Google Cloud Storage失败问题求助的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


你的代码存在几个关键问题导致文件无法上传到Google Cloud Storage:

  1. 文件指针位置问题:在写入本地文件后,文件指针位于末尾,需要重置
  2. GCS写入器未正确关闭:必须调用w.Close()来提交上传
  3. 本地文件处理逻辑问题f == nil检查永远不会为真

以下是修复后的代码:

func uploadFile(c *gin.Context) {
    // 获取上传的文件
    file, header, err := c.Request.FormFile("file")
    if err != nil {
        c.JSON(400, gin.H{"error": "无法获取文件"})
        return
    }
    defer file.Close()

    // 创建本地临时文件
    tempFile, err := os.CreateTemp("", "upload-*")
    if err != nil {
        c.JSON(500, gin.H{"error": "创建临时文件失败: " + err.Error()})
        return
    }
    defer os.Remove(tempFile.Name())
    defer tempFile.Close()

    // 写入临时文件
    if _, err := io.Copy(tempFile, file); err != nil {
        c.JSON(500, gin.H{"error": "写入临时文件失败: " + err.Error()})
        return
    }

    // 重置文件指针到开头
    if _, err := tempFile.Seek(0, io.SeekStart); err != nil {
        c.JSON(500, gin.H{"error": "重置文件指针失败: " + err.Error()})
        return
    }

    // 检查文件是否上传完成
    if isFileUploadCompleted(c) {
        uploadToGoogle(c, tempFile, header.Filename)
    } else {
        c.JSON(200, gin.H{"status": "分块接收成功"})
    }
}

func uploadToGoogle(c *gin.Context, f *os.File, filename string) {
    // 验证环境变量
    if _, err := os.Stat(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")); err != nil {
        c.JSON(500, gin.H{"error": "GCP凭证文件不存在: " + err.Error()})
        return
    }

    const bucketName = "el-my-gallery"

    // 创建存储客户端
    ctx := context.Background()
    client, err := storage.NewClient(ctx)
    if err != nil {
        c.JSON(500, gin.H{"error": "创建GCS客户端失败: " + err.Error()})
        return
    }
    defer client.Close()

    // 创建存储桶句柄和写入器
    bucket := client.Bucket(bucketName)
    obj := bucket.Object(filename)
    w := obj.NewWriter(ctx)
    
    // 设置内容类型(重要)
    w.ContentType = "application/octet-stream"
    
    // 复制文件内容到GCS
    if _, err := io.Copy(w, f); err != nil {
        w.Close()
        c.JSON(500, gin.H{"error": "上传到GCS失败: " + err.Error()})
        return
    }

    // 必须关闭写入器以提交上传
    if err := w.Close(); err != nil {
        c.JSON(500, gin.H{"error": "关闭GCS写入器失败: " + err.Error()})
        return
    }

    // 验证文件是否成功上传
    attrs, err := obj.Attrs(ctx)
    if err != nil {
        c.JSON(500, gin.H{"error": "验证上传失败: " + err.Error()})
        return
    }

    fmt.Printf("文件 %s 成功上传到GCS,大小: %d 字节\n", filename, attrs.Size)
    c.JSON(200, gin.H{
        "status": "上传成功",
        "filename": filename,
        "size": attrs.Size,
        "bucket": bucketName,
    })
}

主要修复点:

  1. 使用临时文件:避免并发写入问题
  2. 重置文件指针f.Seek(0, io.SeekStart)在复制前执行
  3. 关闭GCS写入器w.Close()是必需的,否则上传不会完成
  4. 添加错误处理:使用c.JSON()返回错误而不是panic
  5. 正确的上下文传递:使用context.Background()而不是gin的上下文
  6. 验证上传结果:通过obj.Attrs()确认文件已存在

分块上传的完整处理示例:

// 用于跟踪分块上传的状态
type uploadSession struct {
    TempFile *os.File
    FileName string
    Received int64
    TotalSize int64
    mu sync.Mutex
}

var sessions = make(map[string]*uploadSession)
var sessionsMu sync.Mutex

func uploadChunk(c *gin.Context) {
    // 获取上传ID(前端应提供)
    uploadID := c.Query("upload_id")
    if uploadID == "" {
        uploadID = uuid.New().String()
    }

    sessionsMu.Lock()
    session, exists := sessions[uploadID]
    if !exists {
        // 创建新会话
        tempFile, err := os.CreateTemp("", fmt.Sprintf("upload-%s-*", uploadID))
        if err != nil {
            sessionsMu.Unlock()
            c.JSON(500, gin.H{"error": err.Error()})
            return
        }
        
        session = &uploadSession{
            TempFile: tempFile,
            FileName: c.Query("filename"),
        }
        sessions[uploadID] = session
    }
    sessionsMu.Unlock()

    // 处理分块
    file, _, err := c.Request.FormFile("file")
    if err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    defer file.Close()

    session.mu.Lock()
    defer session.mu.Unlock()

    // 追加分块到临时文件
    n, err := io.Copy(session.TempFile, file)
    if err != nil {
        c.JSON(500, gin.H{"error": err.Error()})
        return
    }
    
    session.Received += n
    
    // 获取总文件大小
    contentRange := c.GetHeader("Content-Range")
    if contentRange != "" {
        parts := strings.Split(contentRange, "/")
        if len(parts) == 2 {
            if total, err := strconv.ParseInt(parts[1], 10, 64); err == nil {
                session.TotalSize = total
            }
        }
    }

    // 检查是否完成
    if session.TotalSize > 0 && session.Received >= session.TotalSize {
        // 重置文件指针并上传
        session.TempFile.Seek(0, io.SeekStart)
        uploadToGoogle(c, session.TempFile, session.FileName)
        
        // 清理会话
        sessionsMu.Lock()
        delete(sessions, uploadID)
        sessionsMu.Unlock()
        
        session.TempFile.Close()
        os.Remove(session.TempFile.Name())
    } else {
        c.JSON(200, gin.H{
            "upload_id": uploadID,
            "received": session.Received,
            "total": session.TotalSize,
            "status": "chunk_received",
        })
    }
}

这些修复确保文件正确上传到Google Cloud Storage,并提供适当的分块处理机制。

回到顶部