先说说心路历程,哈哈哈。
其实昨天踩点发文章,也是因为在倒腾这些。
我之前写过一篇文章,通过使用upgit来自动上传typora的图片。
当时,用的是github,从这个项目的名字也能看出,它应该最开始就是为github写的,所以支持的最好。
其实github本身没什么大问题,但因为众所周知的原因,网络是个大问题,即便我挂了梯子,也会有不好使的时候。
所以,我准备换。
切换Gitee
看了一下它的可扩展列表
gitee码云,应该是最合适的替代品,首先它是国内的产品,其次,它也是一个基于git的版本控制工具。我可以直接把github上的项目克隆到gitee上,把我所有图片的前缀url换一下就好了。
于是,我美滋滋的开始换了,但是,gitee给了我一个巨大的惊喜,她说因为检查到我有许多图片外链,怀疑我使用它做图床,不允许我把这个项目设置成公开权限,也就是说,图片我可以上传,但是我不能访问。那我要它还有什么用?
切换七牛云
既然gitee用不了,我总得换,再次浏览了一遍扩展列表,就七牛云比较熟悉,其他的也不知道会不会满足我的要求。既然有一个确定能满足我要求的,那就直接用它得了。
然后,就开始了我的踩坑之旅,哈哈哈。
第一件事,就是配置文件的修改。
这块我觉得它文档写得不太详细,我是看了日志,然后又去扫了一遍源码,才知道是个怎么回事的。
按照正常逻辑,我觉得我把这个默认上传器给改了,然后把它相应的配置加上就能用了。
可惜并不能,会报错:
可能是因为一叶障目,我开始并没有意识到这个错误是说D:\Program Files\upgit\extensions这个文件夹找不到。我光看到冒号后面的信息了,我因为它说找不到要上传的文件,就上面灰色的那些,我去文件夹里看了一眼,是有的。
后来左看右看,在日志里终于是发现了问题,于是赶紧把文件夹加上吧。
但还是报错了:
我一时间觉得很费解,难道是我的名字输错啦?那要是输错了,应该输入什么呢?我也没什么头绪。
所幸去源码里搜了一下,这个unknown uploader是从哪里报出来的。
于是,终于发现了问题,它需要一个相关的配置文件。
并且源码里有:
赶紧把我需要的copy下来。(我感觉吧,这些配置文件,理论上应该直接放在release包里的,还要自己去建文件夹,复制配置文件,感觉怪怪的,文档里提一句也行。)
这时候,故事才刚刚开始,之前提到的github、gitee都可以创建一个永久的token,这样把token写进配置文件中,就再也不用改了。
但是,七牛云可能是因为上传是免费的,下载才要钱的原因,所以对上传把控的比较严,所以,并没有永久的token,token有时效,过了时间需要重新请求token。
那么问题来了,upgit只提供了配置token的方式,并不支持刷新token。
这个时候,我陷入了天人交战。
放弃upgit,自己写一套基于七牛云的文件上传的具?感觉又犯不着。继续使用upgit,修改一下七牛云的逻辑?但是upgit是用go语言写的,我对go并不熟。
几番权衡之下,我感觉还是直接改源码比较快,虽然,我对go不熟,但是,我看了一眼七牛云的官方文档,有对go语言的支持,获取token还挺简单的:
签名那些东西,不用我自己写。所以,改动还是比较小的。
我只用把配置文件里的token改成accessKey、secretKey、bucket,然后在代码里把原来获取token的地方,换成我新生成的token就行了。
虽然,我对go的语法不熟,也不太明白整个upgit的运行逻辑和框架。但是,我其实只用改获取配置文件那一块就好了。
想好了,就开干吧。
修改upgit源码
还记得我前面说找unknown uploader报错从哪报的吗?就在这个方法里,虽然看不懂语法细节,但能大体知道这是在干嘛,根据uploaderId来判断要用哪个load,前面几个if应该都是之前特别实现的。
剩下的那些没有特别if的,都归到了extensions里面:
然后,我就照葫芦画瓢,这里抄点代码,哪里抄点代码,进行了如下修改:
首先,导包:
然后,判断一下,如果是使用七牛云,就获取token
然后,把token放进config里
从配置文件中,能看到token是通过这种方式获取的:
那我其实替换一下token的值就行了
(我到时候,直接把代码放到文章最末尾,省得排版太长)
至此,改动结束。
其实,我不是太敢做一些结构上的修改。现在这么改一点都不优雅,但小步子总是没错的,先验证自己的想法没有问题,再优化也好,现在的目的仅仅是正确地运行。
代码改完了,下一步就是编译啦,要让它变成可执行文件。
我看到根目录上有一个Makefile文件,但是windows并没有GCC编译工具,于是,要先安装,我装的是MinGW,参考的这篇文章,我就不详细说了:
https://blog.csdn.net/LinusZhao1018/article/details/82152960?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param
装好之后,到项目更目录,执行make命令,但是,报错了:
查了一下,没有很快找到答案,我也不觉得真的是语法的问题,很可能是系统的问题,可能我是windows系统,作者是在linux上运行的也说不定。懒得去研究makefile的语法问题了。
看得出来真正有意义的是go build……这句话,那我自己按照我的系统拼一条命令出来不就行啦?
于是就有了:
go build -o ./dist/upgit_win_amd64.exe -ldflags="-s -w" .
运行!
不出所料,又报错啦
ext_cmd.go:11:2: github.com/alexflint/go-arg@v1.4.3: Get "https://proxy.golang.org/github.com/alexflint/go-arg/@v/v1.4.3.zip": dial tcp 142.251.43.17:443: connectex: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.
连接超时,把我的梯子打开,也没有用。google了一下,看起来是很多人都会遇到的问题,把访问地址改成国内的就行:
go env -w GOPROXY=https://goproxy.cn
再次运行,还是报错:
ain.go:32:2: no required module provides package github.com/qiniu/go-sdk/v7/auth/qbox;to add it:
go get github.com/qiniu/go-sdk/v7/auth/qbox
main.go:33:5: no required module provides package github.com/qiniu/go-sdk/v7/storage;to add it:
go get github.com/qiniu/go-sdk/v7/storage
接着google,因为我没有在go.mod文件添加相应的require版本:
require github.com/qiniu/go-sdk/v7 v7.13.0
再次运行,还是报错,哈哈哈:
main.go:32:2: missing go.sum entry for module providing package github.com/qiniu/go-sdk/v7/auth/qbox (imported by github.com/pluveto/upgit);to add:
go get github.com/pluveto/upgit
main.go:33:5: missing go.sum entry for module providing package github.com/qiniu/go-sdk/v7/storage (imported by github.com/pluveto/upgit);to add:
go get github.com/pluveto/upgit
再次google,好像是光修改go.mod文件不够,还需要运行下面的命令,来更新:
go mod tidy
然后再次运行,虽然还是报错了,但和环境无关了,终于和我的代码有关了,我的语法有些问题。
这么简单分享一点,今天学到的go语法相关知识。
1.:=与=
:=表示初始化,并赋值
=表示单纯的赋值
2.go的方法,参数在前,类型在后
3.go可以返回多个返回值
原本下面这句话,我抄过来的时候,把err给去掉了
但是报错了:
# github.com/pluveto/upgit
.\main.go:329:16: assignment mismatch: 1 variable but xapp.LoadUploaderConfig[map[string]interface{}] returns 2 values
4.map值的获取与设置(跟python一样)
获取值:qiniuConfig["bucket"]
设置值:extConfig["token"]=upToken
5.go的类型转换是.(type)放在后面的形式
原本我是这么写的:
bucket:=qiniuConfig["bucket"]
accessKey:=qiniuConfig["accessKey"]
secretKey:=qiniuConfig["secretKey"]
但是报错了:
.\main.go:334:12: cannot use bucket (variable of type interface{}) as type string in struct literal:
need type assertion
.\main.go:336:22: cannot use accessKey (variable of type interface{}) as type string in argument to qbox.NewMac:
need type assertion
.\main.go:336:33: cannot use secretKey (variable of type interface{}) as type string in argument to qbox.NewMac:
need type assertion
于是,转成string类型:
bucket:=qiniuConfig["bucket"].(string)
accessKey:=qiniuConfig["accessKey"].(string)
secretKey:=qiniuConfig["secretKey"].(string)
至此,我的程序可算是成功编译了。
还有另外一个令人高兴的事情是,我一次就改成功了。
我把配置文件改好之后,就能正常使用了,这篇文章里的图片,就是传到七牛云的。
配置文件:
config.toml
# 默认上传器
default_uploader="qiniu"
# 七牛云存储
[uploaders.qiniu]
bucket="moqian-public"
accessKey="xxxxxxxxxxxxxxxxxxx"
secretKey="xxxxxxxxxxxxxxxxx"
#你的域名前缀
prefix="http://file.moqian.cn/"
qiniu.jsonc
这个其实用源码里的就好了,只有一个地方可能要改,这个url,上传失败的话,返回的信息会告诉你,应该用哪个url的。
最后放上main.go的代码:
package main
import(
"errors"
"fmt"
"io/fs"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"github.com/alexflint/go-arg"
"github.com/pelletier/go-toml/v2"
"github.com/pluveto/upgit/lib/model"
"github.com/pluveto/upgit/lib/qcloudcos"
"github.com/pluveto/upgit/lib/result"
"github.com/pluveto/upgit/lib/uploaders"
"github.com/pluveto/upgit/lib/upyun"
"github.com/pluveto/upgit/lib/xapp"
"github.com/pluveto/upgit/lib/xclipboard"
"github.com/pluveto/upgit/lib/xext"
"github.com/pluveto/upgit/lib/xio"
"github.com/pluveto/upgit/lib/xlog"
"github.com/pluveto/upgit/lib/xmap"
"github.com/pluveto/upgit/lib/xpath"
"github.com/pluveto/upgit/lib/xstrings"
"golang.design/x/clipboard"
"gopkg.in/validator.v2"
"github.com/qiniu/go-sdk/v7/auth/qbox"
"github.com/qiniu/go-sdk/v7/storage"
)
func main(){
result.AbortErr = xlog.AbortErr
iflen(os.Args)>=2&& os.Args[1]=="ext"{
extSubcommand()
return
}
mainCommand()
}
func mainCommand(){
// parse cli args
loadCliOpts()
// load config
loadEnvConfig(&xapp.AppCfg)
loadConfig(&xapp.AppCfg)
xlog.GVerbose.TraceStruct(xapp.AppCfg)
// handle clipboard if need
loadClipboard()
// validating args
validArgs()
// executing uploading
dispatchUploader()
if xapp.AppOpt.Wait {
fmt.Scanln()
}
return
}
// loadCliOpts load cli options into xapp.AppOpt
func loadCliOpts(){
arg.MustParse(&xapp.AppOpt)
xapp.AppOpt.TargetDir = strings.Trim(xapp.AppOpt.TargetDir,"/")
xapp.AppOpt.ApplicationPath = strings.Trim(xapp.AppOpt.ApplicationPath,"/")
iflen(xapp.AppOpt.ApplicationPath)>0{
xpath.ApplicationPath = xapp.AppOpt.ApplicationPath
}
if xapp.AppOpt.SizeLimit !=nil&&*xapp.AppOpt.SizeLimit >=0{
xapp.MaxUploadSize =*xapp.AppOpt.SizeLimit
}
iffalse== xapp.AppOpt.NoLog {
xlog.GVerbose.LogEnabled =true
xlog.GVerbose.LogFile = xpath.MustGetApplicationPath("upgit.log")
xlog.GVerbose.LogFileMaxSize =2*1024*1024// 2MiB
xlog.GVerbose.Info("Started")
xlog.GVerbose.TruncatLog()
}
xlog.GVerbose.VerboseEnabled = xapp.AppOpt.Verbose
xlog.GVerbose.TraceStruct(xapp.AppOpt)
}
func onUploaded(r result.Result[*model.Task]){
if!r.Ok()&& xapp.AppOpt.OutputType == xapp.O_Stdout {
fmt.Println("Failed: "+ r.Err.Error())
return
}
if xapp.AppOpt.Clean &&!r.Value.Ignored {
err := os.Remove(r.Value.LocalPath)
if err !=nil{
xlog.GVerbose.Info("Failed to remove %s: %s", r.Value.LocalPath, err.Error())
}else{
xlog.GVerbose.Info("Removed %s", r.Value.LocalPath)
}
}
outputLink(*r.Value)
recordHistory(*r.Value)
}
func mustMarshall(s interface{})string{
b, err := toml.Marshal(s)
if err !=nil{
return""
}
returnstring(b)
}
func recordHistory(r model.Task){
xio.AppendToFile(xpath.MustGetApplicationPath("history.log"),[]byte(
`{"time":"`+time.Now().Local().String()+`","rawUrl":"`+r.RawUrl+`","url":"`+r.Url+`"}`+"\n"),
)
xlog.GVerbose.Info(mustMarshall(r))
}
func outputLink(r model.Task){
outContent, err := outputFormat(r)
xlog.AbortErr(err)
switch xapp.AppOpt.OutputType {
case xapp.O_Stdout:
fmt.Println(outContent)
case xapp.O_Clipboard:
clipboard.Write(clipboard.FmtText,[]byte(outContent))
default:
xlog.AbortErr(errors.New("unknown output type: "+string(xapp.AppOpt.OutputType)))
}
}
func outputFormat(r model.Task)(content string, err error){
var outUrl string
if xapp.AppOpt.Raw || r.Url ==""{
outUrl = r.RawUrl
}else{
outUrl = r.Url
}
fmt := xapp.AppOpt.OutputFormat
if fmt ==""{
return outUrl,nil
}
val, ok := xapp.AppCfg.OutputFormats[fmt]
if!ok {
return"", errors.New("unknown output format: "+ fmt)
}
content = strings.NewReplacer(
"{url}", outUrl,
"{urlfname}", filepath.Base(outUrl),
"{fname}", filepath.Base(r.LocalPath),
).Replace(xstrings.RemoveFmtUnderscore(val))
return
}
func validArgs(){
if errs := validator.Validate(xapp.AppCfg); errs !=nil{
xlog.AbortErr(fmt.Errorf("incorrect config: "+ errs.Error()))
}
for _, path :=range xapp.AppOpt.LocalPaths {
if strings.HasPrefix(path,"http"){
continue
}
fs, err := os.Stat(path)
if errors.Is(err, os.ErrNotExist){
xlog.AbortErr(fmt.Errorf("invalid file to upload %s: no such file", path))
}
if err !=nil{
xlog.AbortErr(fmt.Errorf("invalid file to upload %s: %s", path, err.Error()))
}
if fs.Size()==0{
xlog.AbortErr(fmt.Errorf("invalid file to upload %s: file size is zero", path))
}
if xapp.MaxUploadSize !=0&& fs.Size()> xapp.MaxUploadSize {
xlog.AbortErr(fmt.Errorf("invalid file to upload %s: file size is larger than %d bytes", path, xapp.MaxUploadSize))
}
}
}
// loadConfig loads config from config file to xapp.AppCfg
func loadConfig(cfg *xapp.Config){
homeDir, err := os.UserHomeDir()
if err !=nil{
homeDir =""
}
appDir := xpath.MustGetApplicationPath("")
var configFiles =map[string]bool{
filepath.Join(homeDir,".upgit.config.toml"):false,
filepath.Join(homeDir, filepath.Join(".config","upgitrc")):false,
filepath.Join(appDir,"config.toml"):false,
filepath.Join(appDir,"upgit.toml"):false,
}
if xapp.AppOpt.ConfigFile !=""{
configFiles[xapp.AppOpt.ConfigFile]=true
}
for configFile, required :=range configFiles {
if _, err := os.Stat(configFile); err !=nil{
if required {
xlog.AbortErr(fmt.Errorf("config file %s not found", configFile))
}
continue
}
optRawBytes, err := ioutil.ReadFile(configFile)
if err ==nil{
err = toml.Unmarshal(optRawBytes,&cfg)
}
if err !=nil{
xlog.AbortErr(fmt.Errorf("invalid config: "+ err.Error()))
}
xapp.ConfigFilePath = configFile
break
}
if xapp.ConfigFilePath ==""{
xlog.AbortErr(fmt.Errorf("no config file found"))
}
// fill config
xapp.AppCfg.Rename = strings.Trim(xapp.AppCfg.Rename,"/")
xapp.AppCfg.Rename = xstrings.RemoveFmtUnderscore(xapp.AppCfg.Rename)
// -- integrated formats
ifnil== xapp.AppCfg.OutputFormats {
xapp.AppCfg.OutputFormats =make(map[string]string)
}
xapp.AppCfg.OutputFormats["markdown"]=`![{url_fname}]({url})`
xapp.AppCfg.OutputFormats["url"]=`{url}`
}
// UploadAll will upload all given file to targetDir.
// If targetDir is not set, it will upload using rename rules.
func UploadAll(uploader model.Uploader, localPaths []string, targetDir string){
for taskId, localPath :=range localPaths {
var ret result.Result[*model.Task]
task := model.Task{
Status: model.TASK_CREATED,
TaskId: taskId,
LocalPath: localPath,
TargetDir: targetDir,
RawUrl:"",
Url:"",
CreateTime: time.Now(),
}
var err error
// ignore non-local path
if strings.HasPrefix(localPath,"http")