Skip to content

Commit

Permalink
上传初版,实现目标功能
Browse files Browse the repository at this point in the history
  • Loading branch information
luckykeeper committed Jun 6, 2023
1 parent 6f748da commit 02a8f21
Show file tree
Hide file tree
Showing 5 changed files with 309 additions and 0 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
# typoraUploader
Typora 图片上传插件,适配 NoaHandler 对象存储网关组件(后端:Minio)

## usage

```powershell
./typoraUploader.exe upload $filePath
```
9 changes: 9 additions & 0 deletions config.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# typoraUploader Settings
# Powered By Luckykeeper <[email protected] | https://luckykeeper.site>

[typoraUploader]
noaHandlerAddr = "" ;API 服务端口
token = "" ;API 服务 Token
bucket= "" ;存储桶的名称
workflow = "" ;API 服务的任务流
storageType = "" ;存储桶类型,如"s3"
16 changes: 16 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module typoraUploader

go 1.20

require (
github.com/h2non/filetype v1.1.3
github.com/urfave/cli/v2 v2.25.5
gopkg.in/ini.v1 v1.67.0
)

require (
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/stretchr/testify v1.8.4 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
)
17 changes: 17 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/urfave/cli/v2 v2.25.5 h1:d0NIAyhh5shGscroL7ek/Ya9QYQE0KNabJgiUinIQkc=
github.com/urfave/cli/v2 v2.25.5/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
261 changes: 261 additions & 0 deletions typoraUploader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
package main

import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"

"github.com/h2non/filetype"
"github.com/urfave/cli/v2"
"gopkg.in/ini.v1"
)

var (
noaHandlerAddr, NoaHandlerToken, Bucket, Workflow, StorageType string
)

func typoraUploaderCLI() {
typoraUploader := &cli.App{
Name: "typoraUploader",
Usage: "typora 图片上传插件,上传图片到 NoaHandler 平台" +
"\nPowered By Luckykeeper <[email protected] | https://luckykeeper.site>" +
"\n————————————————————————————————————————" +
"\n注意:使用前需要先填写同目录下 config.ini !",
Version: "1.0.0_build20230606",
Commands: []*cli.Command{
{
Name: "upload",
Aliases: []string{"u"},
Usage: "上传文件到指定路径",
Action: func(cCtx *cli.Context) error {

exeFilePath, _ := os.Executable()
workDir := exeFilePath[:strings.LastIndex(exeFilePath, "\\")]
workDir = strings.ReplaceAll(workDir, "\\", "/")

readConfig(workDir)
// 判断 webp 依赖库
if bool, _ := pathExists(workDir + "/libwebp/bin/cwebp.exe"); !bool {
log.Fatalln("未找到\"" + workDir + "/libwebp/bin/cwebp.exe\"\n请先下载 webp 依赖库,并放置在指定位置!")
}
// 转换上传任务流
uploadPathList := cCtx.Args().Slice()
filePathList, deleteList := picWebpWorkflow(uploadPathList, workDir)
fileUrls := uploadToNoaHandler(filePathList)
// 删除缓存文件
for _, file := range deleteList {
os.Remove(file)
}
// 输出结果
for _, file := range fileUrls {
fmt.Println(file)
}
return nil
},
},
},
}

if err := typoraUploader.Run(os.Args); err != nil {
log.Fatal(err)
}
}

// read config.ini
func readConfig(workDir string) {
configFile, err := ini.Load(workDir + "/config.ini")
if err != nil {
log.Panicln("读取配置文件 config.ini 失败,失败原因为: ", err)
os.Exit(1)
}
noaHandlerAddr = configFile.Section("typoraUploader").Key("noaHandlerAddr").String()
NoaHandlerToken = configFile.Section("typoraUploader").Key("token").String()
Bucket = configFile.Section("typoraUploader").Key("bucket").String()
Workflow = configFile.Section("typoraUploader").Key("workflow").String()
StorageType = configFile.Section("typoraUploader").Key("storageType").String()
}

func main() {
typoraUploaderCLI()
}

// 图片判断与转换任务流,返回图片路径及要删除的文件路径,若转换下一环节上传完应当删除转换过的文件
func picWebpWorkflow(uploadPathList []string, workDir string) (filePathList, deleteList []string) {
for i, filePath := range uploadPathList {
// 判断文件类型,若为 webp 可直接上传
needConvert := checkFileHeader(filePath)
if needConvert {
// 对图像进行 webp 转换
// .\libwebp\bin\cwebp.exe -q 75 -m 6 -mt .\E2E912F7717BF4FD0BBFF615207F6CFC.jpg -o .\E2E912F7717BF4FD0BBFF615207F6CFC.webp
newFileName := workDir + "/" + strconv.Itoa(i+1) + ".webp"
// 注意参数不能用空格分开,如:-q 75 是错误的
command := exec.Command(workDir+"/libwebp/bin/cwebp.exe", "-q", "75", "-m", "6", "-mt", filePath, "-o", newFileName)
command.Run()

deleteList = append(deleteList, newFileName)
filePathList = append(filePathList, newFileName)
} else {
// 直接上传,且不进入删除列表
filePathList = append(filePathList, filePath)
}
}
return
}

// 上传任务流
func uploadToNoaHandler(uploadList []string) (picUrls []string) {
for _, file := range uploadList {
for {
serverReturn := uploadFileToUshioNoa(file)
if serverReturn.StatusCode != 200 {
continue
} else {
picUrls = append(picUrls, serverReturn.FileUrl)
break
}
}
}
return
}

// 检查文件类型
func checkFileHeader(filePath string) (needConvert bool) {
// buf, _ := ioutil.ReadFile(filePath)
buf, _ := os.ReadFile(filePath)
kind, _ := filetype.Match(buf)
fileType := kind.Extension

// 白名单
// 图片:jpg png webp
if fileType == "jpg" || fileType == "png" {
needConvert = true
return
} else if fileType == "webp" {
needConvert = false
return
} else {
log.Fatalln("UnSupported FlieType! Or File No Exists!")
return
}
}

// 判断文件是否存在
func pathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}

// post 方式上传文件(单文件)
// 根据现成的封装再封一层
func uploadFileToUshioNoa(filePath string) (serverReturn UshioNoaUploadFileResult) {

file := []UploadFile{
{Name: "file", Filepath: filePath},
}

gatewayRequestUrl := noaHandlerAddr + "uploadFile"

reqParams := map[string]string{"token": NoaHandlerToken,
"storageType": "s3",
"bucket": Bucket,
"workFlow": Workflow}
response := PostFile(gatewayRequestUrl, reqParams, file, map[string]string{"User-Agent": "typoraUploader"})
json.Unmarshal(response, &serverReturn)
return serverReturn
}

type UploadFile struct {
// 表单名称
Name string
// 文件全路径
Filepath string
}

// 请求客户端
var httpClient = &http.Client{}

func PostFile(reqUrl string, reqParams map[string]string, files []UploadFile, headers map[string]string) []byte {
return post(reqUrl, reqParams, "multipart/form-data", files, headers)
}

func post(reqUrl string, reqParams map[string]string, contentType string, files []UploadFile, headers map[string]string) []byte {
requestBody, realContentType := getReader(reqParams, contentType, files)
httpRequest, _ := http.NewRequest("POST", reqUrl, requestBody)
// 添加请求头
httpRequest.Header.Add("Content-Type", realContentType)
for k, v := range headers {
httpRequest.Header.Add(k, v)
}
// 发送请求
resp, err := httpClient.Do(httpRequest)
if err != nil {
panic(err)
}
defer resp.Body.Close()
response, _ := io.ReadAll(resp.Body)
return response
}

func getReader(reqParams map[string]string, contentType string, files []UploadFile) (io.Reader, string) {
if strings.Contains(contentType, "json") {
bytesData, _ := json.Marshal(reqParams)
return bytes.NewReader(bytesData), contentType
} else if files != nil {
body := &bytes.Buffer{}
// 文件写入 body
writer := multipart.NewWriter(body)
for _, uploadFile := range files {
file, err := os.Open(uploadFile.Filepath)
if err != nil {
panic(err)
}
part, err := writer.CreateFormFile(uploadFile.Name, filepath.Base(uploadFile.Filepath))
if err != nil {
panic(err)
}
io.Copy(part, file)
file.Close()
}
// 其他参数列表写入 body
for k, v := range reqParams {
if err := writer.WriteField(k, v); err != nil {
panic(err)
}
}
if err := writer.Close(); err != nil {
panic(err)
}
// 上传文件需要自己专用的contentType
return body, writer.FormDataContentType()
} else {
urlValues := url.Values{}
for key, val := range reqParams {
urlValues.Set(key, val)
}
reqBody := urlValues.Encode()
return strings.NewReader(reqBody), contentType
}
}

// 文件上传接口 - 返回
type UshioNoaUploadFileResult struct {
StatusCode int `json:"statusCode"` // 结果码 (操作成功200,Token错误401)
StatusString string `json:"StatusString"`
FileUrl string `json:"fileUrl"`
}

0 comments on commit 02a8f21

Please sign in to comment.