Golang实现文件上传到互联网的方法指南

Golang实现文件上传到互联网的方法指南 使用以下代码,我可以从互联网下载文件并监控下载进度。

如何实现上传文件到互联网并监控上传进度?我想将可执行文件上传到 GitHub 的 Assets 部分。

package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"

	"github.com/dustin/go-humanize"
)

// WriteCounter 用于统计写入的字节数。它实现了 io.Writer 接口,
// 我们可以将其传递给 io.TeeReader(),后者将在每次写入周期报告进度。
type WriteCounter struct {
	Total uint64
}

func (wc *WriteCounter) Write(p []byte) (int, error) {
	n := len(p)
	wc.Total += uint64(n)
	wc.PrintProgress()
	return n, nil
}

func (wc WriteCounter) PrintProgress() {
	// 使用回车符回到行首并填充空格来清除该行
	fmt.Printf("\r%s", strings.Repeat(" ", 35))

	// 再次回车并打印当前的下载状态
	// 我们使用 humanize 包以更易读的方式打印字节数(例如 10 MB)
	fmt.Printf("\rDownloading... %s complete", humanize.Bytes(wc.Total))
}

func main() {
	fmt.Println("Download Started")

	fileUrl := "https://upload.wikimedia.org/wikipedia/commons/d/d6/Wp-w4-big.jpg"
	err := DownloadFile("avatar.jpg", fileUrl)
	if err != nil {
		panic(err)
	}

	fmt.Println("Download Finished")
}

// DownloadFile 会将一个 URL 下载到本地文件。这种方式很高效,因为它会边下载边写入,
// 而不是将整个文件加载到内存中。我们将一个 io.TeeReader 传递给 Copy() 来报告下载进度。
func DownloadFile(filepath string, url string) error {

	// 创建文件,但使用 .tmp 扩展名,这意味着在下载完成前不会覆盖原文件,
	// 下载完成后我们会移除 .tmp 扩展名。
	out, err := os.Create(filepath + ".tmp")
	if err != nil {
		return err
	}

	// 获取数据
	resp, err := http.Get(url)
	if err != nil {
		out.Close()
		return err
	}
	defer resp.Body.Close()

	// 创建我们的进度报告器,并将其与写入器一起使用
	counter := &WriteCounter{}
	if _, err = io.Copy(out, io.TeeReader(resp.Body, counter)); err != nil {
		out.Close()
		return err
	}

	// 进度显示使用同一行,所以下载完成后需要打印一个换行符
	fmt.Print("\n")

	// 不使用 defer 关闭文件,以便在 Rename() 之前执行
	out.Close()

	if err = os.Rename(filepath+".tmp", filepath); err != nil {
		return err
	}
	return nil
}

更多关于Golang实现文件上传到互联网的方法指南的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

我猜你必须为此使用 GitHub API。

更多关于Golang实现文件上传到互联网的方法指南的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


要实现文件上传到GitHub Assets并监控进度,可以使用以下方法。这里使用GitHub Releases API,并实现类似下载进度的上传进度监控:

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"

	"github.com/dustin/go-humanize"
)

// ReadCounter 用于统计读取的字节数
type ReadCounter struct {
	Total    uint64
	FileName string
}

func (rc *ReadCounter) Read(p []byte) (int, error) {
	n := len(p)
	rc.Total += uint64(n)
	rc.PrintProgress()
	return n, nil
}

func (rc *ReadCounter) PrintProgress() {
	fmt.Printf("\r%s", strings.Repeat(" ", 50))
	fmt.Printf("\rUploading %s... %s complete", rc.FileName, humanize.Bytes(rc.Total))
}

// GitHubReleaseAsset 表示GitHub Release中的资源文件
type GitHubReleaseAsset struct {
	URL      string `json:"url"`
	Name     string `json:"name"`
	Label    string `json:"label,omitempty"`
	State    string `json:"state"`
	Size     int64  `json:"size"`
	MimeType string `json:"content_type,omitempty"`
}

// UploadFileToGitHub 上传文件到GitHub Release的Assets
func UploadFileToGitHub(filePath, repoOwner, repoName, releaseID, githubToken string) error {
	fmt.Println("Upload Started")

	// 打开要上传的文件
	file, err := os.Open(filePath)
	if err != nil {
		return err
	}
	defer file.Close()

	// 获取文件信息
	fileInfo, err := file.Stat()
	if err != nil {
		return err
	}

	// 创建进度计数器
	counter := &ReadCounter{
		FileName: fileInfo.Name(),
	}

	// 创建读取器链:文件 -> 进度监控 -> 最终读取器
	reader := io.TeeReader(file, counter)

	// 构建上传URL
	uploadURL := fmt.Sprintf("https://uploads.github.com/repos/%s/%s/releases/%s/assets",
		repoOwner, repoName, releaseID)

	// 添加文件名参数
	uploadURL += "?name=" + fileInfo.Name()

	// 创建HTTP请求
	req, err := http.NewRequest("POST", uploadURL, reader)
	if err != nil {
		return err
	}

	// 设置请求头
	req.ContentLength = fileInfo.Size()
	req.Header.Set("Authorization", "token "+githubToken)
	req.Header.Set("Content-Type", "application/octet-stream")
	req.Header.Set("Accept", "application/vnd.github.v3+json")

	// 执行上传
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	// 读取响应
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}

	if resp.StatusCode != http.StatusCreated {
		return fmt.Errorf("upload failed: %s\n%s", resp.Status, string(body))
	}

	// 解析响应
	var asset GitHubReleaseAsset
	if err := json.Unmarshal(body, &asset); err != nil {
		return err
	}

	fmt.Printf("\nUpload Finished: %s (%s)\n", asset.Name, humanize.Bytes(uint64(asset.Size)))
	return nil
}

// 使用io.Pipe实现流式上传和进度监控
func UploadFileWithProgress(filePath, uploadURL, authToken string) error {
	fmt.Println("Upload Started")

	file, err := os.Open(filePath)
	if err != nil {
		return err
	}
	defer file.Close()

	fileInfo, err := file.Stat()
	if err != nil {
		return err
	}

	// 创建管道
	pr, pw := io.Pipe()

	// 创建进度计数器
	counter := &ReadCounter{
		FileName: fileInfo.Name(),
	}

	// 在goroutine中读取文件并写入管道
	go func() {
		defer pw.Close()
		_, err := io.Copy(io.MultiWriter(pw, counter), file)
		if err != nil {
			pw.CloseWithError(err)
		}
	}()

	// 创建HTTP请求
	req, err := http.NewRequest("POST", uploadURL, pr)
	if err != nil {
		return err
	}

	req.ContentLength = fileInfo.Size()
	req.Header.Set("Authorization", "Bearer "+authToken)
	req.Header.Set("Content-Type", "application/octet-stream")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("upload failed: %s\n%s", resp.Status, string(body))
	}

	fmt.Println("\nUpload Finished")
	return nil
}

// 使用http.NewRequest和自定义Reader的简化版本
func SimpleUploadWithProgress(filePath, url, token string) error {
	file, err := os.Open(filePath)
	if err != nil {
		return err
	}
	defer file.Close()

	fileInfo, _ := file.Stat()
	
	// 创建自定义Reader来监控进度
	progressReader := &ProgressReader{
		Reader: file,
		Total:  fileInfo.Size(),
		Name:   fileInfo.Name(),
	}

	req, err := http.NewRequest("POST", url, progressReader)
	if err != nil {
		return err
	}

	req.ContentLength = fileInfo.Size()
	req.Header.Set("Authorization", "token "+token)
	req.Header.Set("Content-Type", "application/octet-stream")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	return nil
}

// ProgressReader 实现带进度监控的Reader
type ProgressReader struct {
	Reader io.Reader
	Total  int64
	Read   int64
	Name   string
}

func (pr *ProgressReader) Read(p []byte) (int, error) {
	n, err := pr.Reader.Read(p)
	pr.Read += int64(n)
	
	// 计算并显示进度
	percent := float64(pr.Read) / float64(pr.Total) * 100
	fmt.Printf("\rUploading %s: %s/%s (%.1f%%)",
		pr.Name,
		humanize.Bytes(uint64(pr.Read)),
		humanize.Bytes(uint64(pr.Total)),
		percent)
	
	return n, err
}

func main() {
	// 示例:上传到GitHub Release
	err := UploadFileToGitHub(
		"myapp.exe",           // 要上传的文件
		"yourusername",        // GitHub用户名
		"yourrepo",           // 仓库名
		"12345678",           // Release ID(从GitHub API获取)
		"your_github_token",  // GitHub个人访问令牌
	)
	if err != nil {
		panic(err)
	}

	// 或者使用通用上传方法
	err = SimpleUploadWithProgress(
		"myapp.exe",
		"https://uploads.github.com/repos/owner/repo/releases/123/assets?name=myapp.exe",
		"github_pat_xxx",
	)
	if err != nil {
		panic(err)
	}
}

对于GitHub Assets上传,需要先获取Release ID:

// 获取Release信息以得到Release ID
func GetReleaseID(owner, repo, tag, token string) (string, error) {
	url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/tags/%s", owner, repo, tag)
	
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return "", err
	}
	
	req.Header.Set("Authorization", "token "+token)
	req.Header.Set("Accept", "application/vnd.github.v3+json")
	
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()
	
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	
	var release struct {
		ID int `json:"id"`
	}
	
	if err := json.Unmarshal(body, &release); err != nil {
		return "", err
	}
	
	return fmt.Sprintf("%d", release.ID), nil
}

如果需要处理大文件或需要更精确的控制,可以使用分块上传:

// 分块上传实现
type ChunkedUploader struct {
	File        *os.File
	ChunkSize   int64
	TotalSize   int64
	Uploaded    int64
	FileName    string
	UploadURL   string
	AuthToken   string
}

func (cu *ChunkedUploader) Upload() error {
	buffer := make([]byte, cu.ChunkSize)
	
	for {
		n, err := cu.File.Read(buffer)
		if err != nil && err != io.EOF {
			return err
		}
		
		if n == 0 {
			break
		}
		
		chunk := buffer[:n]
		if err := cu.uploadChunk(chunk); err != nil {
			return err
		}
		
		cu.Uploaded += int64(n)
		cu.printProgress()
		
		if err == io.EOF {
			break
		}
	}
	
	fmt.Println("\nUpload completed")
	return nil
}

func (cu *ChunkedUploader) uploadChunk(chunk []byte) error {
	req, err := http.NewRequest("POST", cu.UploadURL, bytes.NewReader(chunk))
	if err != nil {
		return err
	}
	
	req.Header.Set("Authorization", "Bearer "+cu.AuthToken)
	req.Header.Set("Content-Type", "application/octet-stream")
	req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", 
		cu.Uploaded, cu.Uploaded+int64(len(chunk))-1, cu.TotalSize))
	
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	
	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		return fmt.Errorf("chunk upload failed: %s", resp.Status)
	}
	
	return nil
}

func (cu *ChunkedUploader) printProgress() {
	percent := float64(cu.Uploaded) / float64(cu.TotalSize) * 100
	fmt.Printf("\rUploading %s: %s/%s (%.1f%%)",
		cu.FileName,
		humanize.Bytes(uint64(cu.Uploaded)),
		humanize.Bytes(uint64(cu.TotalSize)),
		percent)
}

这些方法都实现了上传进度监控,可以根据具体需求选择使用。GitHub Assets上传需要有效的个人访问令牌和正确的Release ID。

回到顶部