基于多吉云存储+uPic构建个人图床的实践分享
# 前言
大约从 2021 年开始,我的博客上的图片,都是基于开源的图床程序来构筑的,这个开源的图床程序是:imgurl (opens new window),我平时的使用流程大概是这样:先在本地 Obsidian 将文章写就,然后再往博客的仓库 (opens new window)转移并二次发布,如果文章带有图片,则会通过工具对图片打水印,然后在浏览器访问图床,将图片上传到服务器,再将文章中的图片 URL 换掉。
以上就是我之前的个人图床使用经验,两年来使用还算是比较稳定可靠的。
# 契机
只是最近发现一些问题,就是偶尔程序的登陆状态出现问题,导致自己被关在门外,而无法正常上传图片,另外就是一直以来通过浏览器上传图片的方式也让我感到不方便,个人内心也早早就种下想要换掉图床程序的种子。
我曾经写过一篇有关于图床的文章:与图床相关的各种体验实践 (opens new window),那时候对云存储以及 CDN 这些都还不够了解,所以始终没有把云存储利用起来。
另外一个契机在于:我于今年三月份将博客 CDN 切换到多吉云,并发布了一篇国内 CDN 的评测文章:博客接入CDN的折腾-对阿里云七牛云蓝易云多吉云几家CDN使用评测 (opens new window),在文章里放了个人的邀请链接,并没有想过以此获得什么。然而就在前几天,忽然收到短信,发现上个月有人通过我的链接注册了多吉云,并且购买了云存储的资源包,使得我也获得了两百多的收益 (感谢这位不知名的朋友),这令我感到非常惊喜,也进一步激发我尝试把图床接入到多吉云的云存储。
因此,在介绍我的迁移以及优雅实践之前,再一次分享一下我的邀请链接:
- name: 注册多吉云
desc: 🫡 通过我的邀请链接注册多吉云。
link: https://www.dogecloud.com/?iuid=5485
bgColor: '#6393e2'
textColor: '#242A38'
2
3
4
5
如果你的确有云存储与CDN方面的使用需要,可通过我的邀请链接注册多吉云,那么你第一年的消费我将能拿到一些奖励。
多吉云并没有给我任何广告费用,但我十分愿意推荐多吉云给大家,因为这也是折腾了五六年博客的我经过各种体验评测之后,认为对于个人博客使用者最友好,性价比最好,稳定性也不错的一家服务商。
# 迁移
先说一下基于多吉云云存储建立图床的方式以及访问流程,在云存储里边,创建一个独立的存储空间,然后给这个存储空间绑定一个域名,就可以直接通过这个域名来访问存储空间中的文件了。
因为我之前个人图床的图片都是在服务器本地,因此只需要把图片全量转移到多吉云存储里边。然后路径与原来保持一致,然后将原来图床对应的域名解析改到多吉云的CDN地址即可。
这样只会产生一个存储的费用,以及CDN的流量费用,因此目前我个人博客图片存储量并不大,因此存储的费用每个月几乎可以忽略不计,而流量费用则基本上等同于原来图床域名接入CDN的流量费用,因此整体算来,这还是一个性价比比较高的一次迁移。
# 结合uPic
因为使用云存储了,再像原来每次上传图片还要打开浏览器去上传就比较low了,因此我就打算折腾一下,通过原来一直在用的 uPic (opens new window) 图床上传软件来进行上传的动作的触发。
于是写了一个脚本,起了一个接受图片上传请求,并把图片上传到云存储的服务,通过docker把服务守护在本地,然后借助uPic的自定义上传能力,将整个链路打通。
# 服务脚本
这里把服务脚本简单放在这里,有需要的同学可自行打包部署,当然,如果需要的同学比较多,而且不方便打包,也可以在评论区留言,如果要的人多,我可以花点时间折腾一下,把项目开源,直接做成一键部署的成品。
package main
import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"path/filepath"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/gin-gonic/gin"
)
var (
// 这里替换为你的多吉云永久 AccessKey 和 SecretKey,可在用户中心 - 密钥管理中查看
// 请勿在客户端暴露 AccessKey 和 SecretKey,那样恶意用户将获得账号完全控制权
AccessKey = "xxxxxx"
SecretKey = "xxxxxxxxxxxxxxxxxxxx"
)
func main() {
// 创建gin实例
r := gin.Default()
// 设置路由和处理函数
r.POST("/upload", func(c *gin.Context) {
file, err := c.FormFile("source")
if err != nil {
c.JSON(400, gin.H{
"error": err.Error(),
})
return
}
src, err := file.Open()
if err != nil {
c.JSON(500, gin.H{
"error": err.Error(),
})
}
defer src.Close()
fpath := getFilePath(file.Filename)
_, err = GetS3Client().PutObject(&s3.PutObjectInput{
Bucket: aws.String("s-sh-5485-wiki-images-1258813047"),
Key: aws.String(fpath),
Body: src,
})
if err != nil {
fmt.Printf("文件上传出错,请检查: %v\n", err)
c.JSON(500, gin.H{
"error": err.Error(),
})
}
c.JSON(200, gin.H{
"code": 200,
"url": fpath,
"message": "File uploaded successfully",
})
})
// 启动服务器
r.Run(":666")
}
func getFilePath(name string) string {
formattedTime := time.Unix(time.Now().Unix(), 0).Format("2006/01")
return fmt.Sprintf("imgs/%s/%d%s", formattedTime, time.Now().UnixMilli(), filepath.Ext(name))
}
func DogeCloudAPI(apiPath string, data map[string]interface{}, jsonMode bool) (ret map[string]interface{}) {
body := ""
mime := ""
if jsonMode {
_body, err := json.Marshal(data)
if err != nil {
log.Fatalln(err)
}
body = string(_body)
mime = "application/json"
} else {
values := url.Values{}
for k, v := range data {
values.Set(k, v.(string))
}
body = values.Encode()
mime = "application/x-www-form-urlencoded"
}
signStr := apiPath + "\n" + body
hmacObj := hmac.New(sha1.New, []byte(SecretKey))
hmacObj.Write([]byte(signStr))
sign := hex.EncodeToString(hmacObj.Sum(nil))
Authorization := "TOKEN " + AccessKey + ":" + sign
req, err := http.NewRequest("POST", "https://api.dogecloud.com"+apiPath, strings.NewReader(body))
if err != nil {
fmt.Printf("http request failed : %v\n", err)
}
req.Header.Add("Content-Type", mime)
req.Header.Add("Authorization", Authorization)
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Fatalln(err)
} // 网络错误
defer resp.Body.Close()
r, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("read body failed : %v\n", err)
}
json.Unmarshal([]byte(r), &ret)
return
}
func GetS3Client() *s3.S3 {
prof := make(map[string]interface{})
prof["channel"] = "OSS_FULL"
prof["scopes"] = [1]string{"*"}
r := DogeCloudAPI("/auth/tmp_token.json", prof, true)
data := r["data"].(map[string]interface{})
creds := data["Credentials"].(map[string]interface{})
s3Config := &aws.Config{
Credentials: credentials.NewStaticCredentials(creds["accessKeyId"].(string), creds["secretAccessKey"].(string), creds["sessionToken"].(string)),
Region: aws.String("automatic"),
Endpoint: aws.String("https://cos.ap-shanghai.myqcloud.com"), // 修改为多吉云控制台存储空间 SDK 参数中的 s3Endpoint
}
newSession, err := session.NewSession(s3Config)
if err != nil {
panic(fmt.Errorf("初始化s3客户端失败: %v\n", err))
}
return s3.New(newSession)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
我把这个服务构建为一个镜像,然后每次电脑的 docker 启动之后就会自动运行:
docker run -itd --name doge -p 666:666 doge
此时本地就会有个 666 端口的监听。
# 自定义上传的配置
这里直接通过一张截图来展现配置的详细内容:
- 首先新建一个配置,选择自定义类型。
- 如果你也是在本地运行,则可以填写为:
http://localhost:666/upload
- 该接口为 POST 请求。
- 文件字段名为:source,写死不要更改。
- 这里点击其他字段,要在 header 中增加一下请求类型,
key:content-type
,value:multipart/form-data; charset=utf-8;
- 此处为取文件的存放路径,固定为
["url"]
,不要更改。 - 最后域名则是你绑定在多吉云中的域名。
当我们点击验证,如果返回了上传成功的提示,则说明整个链路通畅了,uPic 也会把图片的路径通过域名与路径拼接起来自动放到粘贴板。
这样就可以愉快地通过 uPic 来高效的完成与图床的交互了。
如果你也想这样折腾,那就赶快行动起来吧,配置过程中有任何问题,都可以在评论区留言,我看到之后一定也会知无不言。