首页 » 工具

告别垃圾 PicGo :用 Go 语言打造 GitHub 图床工具


前言:PicGo 的挣扎与 R2 的“门槛”

最近在搭建自己的技术博客(使用 Typecho),图床是绕不开的话题。市面上大多数教程都推荐使用 PicGo 搭配各种云服务(如 GitHub、七牛云、腾讯云 COS、阿里云 OSS 等)。起初,我也尝试了 PicGo。

然而,一系列问题接踵而至:

  1. NPM 环境报错:尝试安装 PicGo 插件时,控制台不断弹出 Error: spawn npm ENOENTNPM is not installed 的错误。作为 C++/Go 开发者,我实在不想为了一个图床工具专门去配置一套 Node.js 环境。
  2. Cloudflare R2 的“洗礼”:转向 Cloudflare R2,它那免费的 10GB 存储和免费流量费简直是为个人博客量身定制。然而,R2 强制要求绑定付款方式,甚至在我的网络环境下触发了支付“人脸验证”——这无疑给硬核技术者设立了一道不必要的门槛。

面对这些“非代码”问题,我决定回归本源:用 Go 语言自己写一个!

解决方案:Go 语言自制 GitHub 图床工具

我的需求很简单:

  • 能将本地图片上传到云端。
  • 上传后自动复制 Markdown 格式的链接。
  • 部署和使用必须足够简单,最好是一个可执行文件。
  • 免费,且加载速度快。

最终,我选择了 GitHub 仓库 + jsDelivr CDN 的组合,并用 Go 语言编写了一个命令行工具。

1. GitHub 仓库准备

首先,在 GitHub 上创建一个公共仓库,例如 osluck/blog-img。这个仓库将用于存放你的所有博客图片。

接着,生成一个 Personal Access Token (PAT),用于 Go 脚本调用 GitHub API。

  • 进入 GitHub Settings -> Developer settings -> Personal access tokens -> Tokens (classic)
  • 点击 “Generate new token (classic)”
  • Note 填写 blog-img-uploaderExpiration 建议选择 “No expiration”
  • Scopes (权限) 只需勾选 repo
  • 生成后,务必复制并保存生成的 ghp_xxxx 字符串,这将是你的 Go 脚本中 token 的值。

2. Go 语言核心代码

这个 Go 脚本会读取本地图片文件,将其 Base64 编码,然后通过 GitHub API 推送到你的 blog-img 仓库的 uploads/ 目录下。上传成功后,它会自动构造一个 jsd.cdn.zzko.cn 加速链接的 Markdown 格式,并复制到剪贴板。

Go

package main

import (
    "bytes"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
    "path/filepath"
    "time"

    "github.com/atotto/clipboard" // 用于复制到剪贴板
)

func main() {
    // 确保程序接收到图片路径参数
    if len(os.Args) < 2 {
        fmt.Println("用法:请将图片文件拖拽到此程序上")
        time.Sleep(3 * time.Second) // 窗口停留几秒,方便查看提示
        return
    }

    // --- 请修改以下配置为你自己的 GitHub 信息 ---
    const (
        owner = " "          // 你的 GitHub 用户名
        repo  = " "          // 存放图片的仓库名
        token = "你的_GITHUB_TOKEN"  // 刚才生成的 GHP 开头的 Token
    )

    filePath := os.Args[1] // 获取拖拽进来的图片文件路径

    // 读取图片内容
    fileData, err := ioutil.ReadFile(filePath)
    if err != nil {
        fmt.Printf("错误:无法读取文件 %s: %v\n", filePath, err)
        time.Sleep(3 * time.Second)
        return
    }

    // Base64 编码图片内容,以便通过 GitHub API 上传
    base64Content := base64.StdEncoding.EncodeToString(fileData)

    // 构建 GitHub API 上传路径:uploads/时间戳.扩展名
    // 这样可以避免文件名冲突,并按上传时间归类
    fileName := fmt.Sprintf("%d%s", time.Now().Unix(), filepath.Ext(filePath))
    apiUrl := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/uploads/%s", owner, repo, fileName)

    // 构造 PUT 请求的 JSON Payload
    payload := map[string]string{
        "message": "upload by go-cli-uploader", // commit 信息
        "content": base64Content,
    }
    jsonData, err := json.Marshal(payload)
    if err != nil {
        fmt.Printf("错误:JSON 序列化失败: %v\n", err)
        time.Sleep(3 * time.Second)
        return
    }

    // 发送 HTTP PUT 请求到 GitHub API
    req, err := http.NewRequest("PUT", apiUrl, bytes.NewBuffer(jsonData))
    if err != nil {
        fmt.Printf("错误:创建请求失败: %v\n", err)
        time.Sleep(3 * time.Second)
        return
    }
    // 设置 Authorization header,使用 Token 进行身份验证
    req.Header.Set("Authorization", "token "+token)
    req.Header.Set("Content-Type", "application/json") // 必须指定内容类型

    client := &http.Client{}
    resp, err := client.Do(req) // 执行请求
    if err != nil {
        fmt.Printf("错误:HTTP 请求失败: %v\n", err)
        time.Sleep(3 * time.Second)
        return
    }
    defer resp.Body.Close() // 确保关闭响应体

    // 检查 GitHub API 返回的状态码
    if resp.StatusCode == 201 { // 201 Created 表示成功
        //备用链接1 https://jsd.cdn.zzko.cn/gh
        //备用链接2 https://jsd.onmicrosoft.cn/gh
        //全球加速3 https://cdn.jsdelivr.net/gh
        finalURL := fmt.Sprintf("https://jsd.onmicrosoft.cn/gh/%s/%s/uploads/%s", owner, repo, fileName)
        markdown := fmt.Sprintf("![](%s)", finalURL)

        //将Markdown 链接复制到剪贴板
        err = clipboard.WriteAll(markdown)
        if err != nil {
            fmt.Printf("提示:链接已生成但复制到剪贴板失败:%v\n", err)
        }
        fmt.Printf("✅ 图片上传成功!Markdown 链接已复制到剪贴板。\n")
        fmt.Printf("链接:%s\n", finalURL)
    } else {
        // 上传失败,打印错误信息
        respBody, _ := ioutil.ReadAll(resp.Body) // 读取错误响应体
        fmt.Printf("❌ 图片上传失败!HTTP 状态码: %d\n", resp.StatusCode)
        fmt.Printf("GitHub API 响应: %s\n", string(respBody))
    }
    time.Sleep(5 * time.Second) // 延长窗口停留时间,方便调试
}

3. 编译与部署

  1. 初始化 Go Module:在 main.go 所在目录打开终端,执行 go mod init github.com/yuyetufu/blog-uploader (模块名可自定义)。
  2. 安装依赖go mod tidy
  3. 编译程序go build -ldflags="-s -w -H=windowsgui" -o uploader.exe

    • -ldflags="-s -w":减小可执行文件体积。
    • -H=windowsgui":生成无命令行窗口的 GUI 程序(Windows 特有)。
  4. 集成到右键菜单

    • 按下 Win + R 键,输入 shell:sendto 回车。
    • 将编译好的 uploader.exe快捷方式拖入弹出的文件夹中。

4. 使用方式

现在,当你需要上传一张图片到博客时:

  1. 在文件管理器中,找到你的图片。
  2. 右键点击图片 -> 选择 “发送到” -> 点击 “uploader.exe”
  3. 稍等片刻(取决于你的网络速度),程序将在后台完成上传。
  4. 打开你的博客编辑器,直接 Ctrl + V 粘贴,Markdown 格式的图片链接便会显示出来。

结语

一个轻量、高效、完全由自己掌控的图片上传方案。“自己动手,丰衣足食”的优雅,就是开发者而言,无疑是最好的体验。