自定义 typora 的图片上传功能

ShadowC

| 本文阅读量: -

这是 用 Python 给文件名加入 hash 防止冲撞 的后续,现在站点的图片都存在自建的图床里(实际上就是同一台机器里跑的 minio 服务,搭建过程参见 基于 minio 搭建图床服务),又因为 Typora 支持用自定义的方式将图片自动上传到图床,因此简单整理了一个上传、自动加后缀二合一的小程序,这就是 tinyImgUploader

typora 的图片上传规则

参考 Upload Images - Typora Support,自定义上传图片程序的要求是:

  1. typora 调用 /path/to/command "image-path-1" "image-path-2"
  2. 程序输出值的后 n 行被 typora 当作上传图片后得到的链接。

使用方法

  1. 编译二进制程序 go build -o tinyImgUploader.exe --ldflags=" -w -s" .\main.go
  2. 将编译得到的程序放在合适的位置,并在该目录下建立 config.yaml,这个文件设置了图床的位置、用户名及密码,内容参考:
endPoint: "example.com"
accessUser: "your-user"
accessPassword: "your-password"
bucket: "your-bucket"
  1. 在 typora 中设置 - 图像 - 上传服务设定,上传服务选择 Custom Command,命令一栏中填程序的位置。
    • 注:windows 中路径的空格和特殊字符在输入时需要特殊处理,故推荐将程序放在不含空格、中文字符和特殊字符(如括号)的路径下。
  2. 点击 验证图片上传选项 按钮验证设置有效性。

代码简介

  1. 首先从 config.yaml 加载配置;
  2. 根据配置中的信息建立 minio 连接;
  3. 计算文件的 md5 值,并转为 base58 编码(长度更短,可读性更高),附加在文件名中;
  4. 上传到 minio 并拼接访问的字符串。
package main

import (
	"context"
	"crypto/md5"
	"fmt"
	"io"
	"log"
	"mime"
	"os"
	"path/filepath"

	"github.com/btcsuite/btcutil/base58"
	"github.com/minio/minio-go/v7"
	"github.com/minio/minio-go/v7/pkg/credentials"
	"gopkg.in/yaml.v2"
)

type Config struct {
	EndPoint       string `yaml:"endPoint"`
	AccessUser     string `yaml:"accessUser"`
	AccessPassword string `yaml:"accessPassword"`
	Bucket         string `yaml:"bucket"`
}

var config Config

func main() {
	// Load config
	ex, err := os.Executable()
	if err != nil {
		log.Fatalf("cannot get executable: %v\n", err)
		os.Exit(1)
	}
	exPath := filepath.Dir(ex)

	file, err := os.ReadFile(filepath.Join(exPath, "config.yaml"))
	if err != nil {
		log.Fatalf("cannot read config file: %v\n", err)
		os.Exit(1)
	}

	err = yaml.Unmarshal(file, &config)
	if err != nil {
		log.Fatalf("cannot unmarshal config file: %v\n", err)
		os.Exit(1)
	}

	// Initialize minio client object.
	useSSL := true
	client, err := minio.New(config.EndPoint, &minio.Options{
		Creds: credentials.NewStaticV4(
			config.AccessUser, config.AccessPassword, ""),
		Secure: useSSL,
	})
	if err != nil {
		log.Fatalf("cannot connect to minio: %v\n", err)
		os.Exit(1)
	}

	ctx := context.TODO()
	for _, file := range os.Args[1:] {
		ext := filepath.Ext(file)
		fileName := filepath.Base(file)
		fileNameWithoutExt := fileName[:len(fileName)-len(ext)]
		contentType := mime.TypeByExtension(filepath.Ext(file))
		if contentType == "" {
			contentType = "text/plain"
		}
		newName := fmt.Sprintf("%s-%s%s",
			fileNameWithoutExt, GetBase58Md5(file), ext)
		_, err := client.FPutObject(
			ctx, config.Bucket, newName, file,
			minio.PutObjectOptions{ContentType: contentType})
		if err != nil {
			log.Fatalf("failed to upload: %v\n", err)
			return
		}
		fmt.Printf("https://%s/%s/%s\n",
			config.EndPoint, config.Bucket, newName)
	}

}

func GetBase58Md5(filePath string) string {
	file, err := os.Open(filePath)
	if err != nil {
		log.Fatalf("Cannot read file: %v", err)
		return ""
	}

	hash := md5.New()
	if _, err := io.Copy(hash, file); err != nil {
		log.Fatalf("Cannot hash file: %v", err)
		return ""
	}

	return base58.Encode(hash.Sum(nil))
}