实现一个上传文章的命令行工具

这是一个最初的版本,使用 flag 和 ini 来实现文章上传功能。使用 flag 来解析命令行参数,使用 Ini 配置文件,记录识别码,以及 token。记录 token 的原因是因为每次启动命令行,都需要重新获取 token,为了减少 token 获取次数,在获取到 token 后,同时存储到 Ini 配置文件中。每次命令行启动,优先查看配置文件中的 token。

开发这个工具主要有以下几个技术要点:

  • 从命令行获取参数
  • 从配置文件中读取参数
  • 读取文件内容
  • 请求后端接口

我们通过分析后,需要定义以下几个参数:

  • title 标题
  • keyword 关键字
  • filename MD 文件名
  • ispub 是否公开
  • islock 是否加锁

使用 Go 代码定义参数和结构体

var (
	title    string
	keyword  string
	filename string
	ispub    int // 1 pub
	islock   int // 1 lock

)

func init() {
	flag.StringVar(&title, "b", "", "文章题目")
	flag.StringVar(&keyword, "k", "", "文章关键字")
	flag.StringVar(&filename, "f", "", "MD文件")
	flag.IntVar(&islock, "l", 0, "是否加锁")
	flag.IntVar(&ispub, "p", 0, "是否开放")
}

func main(){
    flag.Parse()
}

init() 在 main 函数前调用。需要在 main 函数中调用 flag.Parse(),这一步非常关键。之后就可以使用变量了。

配置文件样例如下:

[User]
icode     = 你的icode
isecret   = 你的isecret
token     =
expire_at = 0

其中,icode 和 isecret 为识别码内容,从小程序端获取。

配置文件库使用gopkg.in/ini.v1,可以读取和写入 ini 文件。

	dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
	if err != nil {
		fmt.Println("获取系统目录错误,", err)
		os.Exit(1)
	}
	confFile := filepath.Join(dir, "conf.ini")
	cfg, err := ini.Load(confFile)
	if err != nil {
		fmt.Println("读取配置文件错误,", err)
		os.Exit(1)
	}
	icode := cfg.Section("User").Key("icode").String()
	isecret := cfg.Section("User").Key("isecret").String()
	token := cfg.Section("User").Key("token").String()
	expireAt := cfg.Section("User").Key("expire_at").MustInt64()

设计的默认配置文件存放在命令行同级目录,但是当在其它目录运行时,会报错配置文件无法找到,为了解决这个问题,需要找到命令行的目录,然后在该目录下再查找配置文件。

为了解决每次运行命令行,都需要重新获取 token,将 token 存储在 ini 配置文件中,并添加超时时间,当在有效时间内,不再重新获取 token。

// 刷新token
	now := time.Now().Unix()
	if now > expireAt {
		token, err = getToken(icode, isecret)
		if err != nil {
			fmt.Println("获取token错误,", err)
			os.Exit(1)
		}
		expireAtStr := strconv.FormatInt(now+7000, 10)
		cfg.Section("User").Key("token").SetValue(token)
		cfg.Section("User").Key("expire_at").SetValue(expireAtStr)
		cfg.SaveTo(confFile)
	}

看看 token 的获取方式,使用 net/http 标准库。


func getToken(icode string, isecret string) (string, error) {
	url := fmt.Sprintf("%s/vtoken?icode=%s&isecret=%s", OPEN_URL, icode, isecret)

	resp, err := http.Get(url)
	if err != nil {
		return "", err
	}

	defer resp.Body.Close()
	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	var result RespData
	err = json.Unmarshal(data, &result)
	if err != nil {
		return "", err
	}
	if result.Code == 1 {
		return result.Data, nil
	} else {
		return "", errors.New(result.Msg)
	}
}

我一般提前编制好 Markdown 文件,然后放在某个目录,然后使用命令工具上传,在上传的时候,就需要读取文件。

filecontent, err := os.ReadFile(filename)
	if err != nil {
		return err
	}

这里实现,简单粗暴,因为文件不会很大,所以一次性读取。当要读取很大的文件时,可以使用缓存,提高性能。

最后就是上传文章接口了。这个实现如下:


func uploadArt(token string, title string, keyword string, filename string, ispub int, islock int) error {
	if token == "" {
		return errors.New("token不能为空")
	}

	url := fmt.Sprintf("%s/upVArt?token=%s", OPEN_URL, token)

	filecontent, err := os.ReadFile(filename)
	if err != nil {
		return err
	}
	snippet := string(filecontent)

	reqBody := new(bytes.Buffer)

	art := ArtReq{
		Title:   title,
		Keyword: keyword,
		Content: snippet,
		IsPub:   ispub,
		IsLock:  islock,
	}
	json.NewEncoder(reqBody).Encode(art)

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

	req.Header.Set("Content-Type", "application/json")
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	var result RespMsg
	err = json.Unmarshal(data, &result)
	if err != nil {
		return err
	}

	if result.Code == 1 {
		return nil
	} else {
		return errors.New(result.Msg)
	}

}

上传功能已经完成。

现在有了新的需求,可以更新文章,删除文章。请看下一章如何实现?