前言

小程序上了云托管后不打算继续使用其他图床,而是使用云托管自带的图床。而由于时间紧迫只能在本地使用云托管提供的服务器调用对象存储 api

云托管上传文件的文档在这里

上传文件文档

如果你上传带有中文名的文件,或者上传的路径有中文名时,需要修改一下 requests 库的源码,否则文件上传成功后无法活动下载链接
修改 requests/models.py 的 prepare_body 里的 complexjson.dumps 参数,加一个 ensure_ascii=False
修改内容

修改内容

流程

获取小程序总体流程可以简单分为下面几步

  • 1、获取调用凭证 access_token
  • 2、获取文件上传消息
  • 3、上传文件
  • 4、获取下载文件的链接

准备 config 文件

因为 access_token 每天都有调用上限,且有 2 小时时限,最好是将 access_token 缓存下来,因此这里使用简单的 json 文件来存

JSON  
1
2
3
4
5
6
7
8
{
    "appid": null, // 小程序appid 必填
    "secret": null, // app secret 必填
    "env": null, // 云托管环境 env
    "create_time": null, // token创建时间 自动生成
    "expires_in": null, // token过期时间 自动生成
    "access_token": null // token 自动生成
}

然后是 config 的读取和保存

PYTHON  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def load_config(path="./config.json"):
    """
    获取配置,主要是读取缓存的access_token
    args: 读取路径
    return: config
    """
    with open(path, "r") as f:
        config = json.load(f)
    return config


def save_config(config, path="./config.json"):
    """
    保持配置
    args: 读取路径
    """
    with open(path, "w") as f:
        json.dump(config, f, indent=4)

获取接口调用凭证

接下来根据我们读取的 config 来获取 access_token

PYTHON  
1
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
def get_access_token(appid: str, secret: str, config) -> str:
    """
    获取调用凭证
    https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html
    args:
        apppid: 小程序appid
        secret: 小程序appSecret

    return: access_token
    """
    # 检查本地token
    token = config["access_token"]
    try:
        now = int(time.time())
        if None == token or (now - config["create_time"]) > config["expires_in"]:
            resp = requests.get(
                "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}".format(
                    appid, secret
                )
            )
            resp = resp.json()

            if None == resp["access_token"]:
                raise Exception(resp["errmsg"])
            # 缓存处理
            config["access_token"] = resp["access_token"]
            config["create_time"] = int(time.time())
            config["expires_in"] = resp["expires_in"]

        return config["access_token"]
    except Exception as e:
        print("获取access token失败")
        raise e

获取云存储上传文件消息

在正式上传前需要获取上传文件的信息,比如上传路径,签名等

PYTHON  
1
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
def get_upload_file_info(access_token: str, env: str, path: str):
    """
    上传文件信息
    https://developers.weixin.qq.com/miniprogram/dev/wxcloudrun/src/development/storage/service/upload.html
    args:
        access_token: 
        env: 云托管的路径
        path: 存放位置
    return:
        json: {
            errcode,
            errmsg,
            url,
            token,
            authorization,
            file_id,
            cos_file_id
        }
    """
    try:
        url = "https://api.weixin.qq.com/tcb/uploadfile?access_token={0}".format(
            access_token
        )
        headers = {"Content-Type": "application/json;charset=UTF-8"}
        data = {"env": env, "path": path}
        resp = requests.post(url, json=data, headers=headers)
        # 转json
        resp = resp.json()
        if 0 != resp["errcode"]:
            print(resp["errmsg"])
            print(resp)
            raise Exception(resp["errmsg"])
        # 返回上传文件信息
        return resp
    except Exception as e:
        print("获取文件上传链接失败")
        raise e

上传文件

获取文件上传信息后,需要根据改信息上传文件

PYTHON  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def upload_file(url, request_path, authorization, token, file_id, file):
    """
    上传文件
    https://developers.weixin.qq.com/miniprogram/dev/wxcloudrun/src/development/storage/service/upload.html
    args:
        url: 上传图片路径 由get_upload获得的url
        request_path: 上传路径,和get_upload_file_info内部的一致
        authorization: info返回的token
        file_id: cos_file_id
        file: 文件的二进制数据
    return: None
    """
    try:
        form = {
            "key": request_path,
            "Signature": authorization,
            "x-cos-security-token": token,
            "x-cos-meta-fileid": file_id,
            "file": file,
        }
        resp = requests.post(url=url, files=form)
        return resp
    except Exception as e:
        print("上传文件失败")
        raise e

获取文件下载链接

上传文件后是没有返回信息的,需要我们根据文件上信息的 file_id 获取

PYTHON  
1
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
def get_download_link(access_token, env, file_id):
    """
    获取文件下载链接
    https://developers.weixin.qq.com/miniprogram/dev/wxcloudrun/src/development/storage/service/download.html
    args:
        access_token: 调用token
        env: 云环境env
        file_id: file_id
    return: 文件下载链接
    """
    try:
        url = "https://api.weixin.qq.com/tcb/batchdownloadfile?access_token={0}".format(
            access_token
        )
        #headers = {"Content-Type": "application/json"}
        data = {
            "env": env,
            "file_list": [
                {
                    "fileid": file_id
                    # max_age
                }
            ],
        }
        resp = requests.post(url, json=data)
        resp = resp.json()
        if 0 != resp["errcode"]:
            print(resp["errmsg"])
            raise Exception(resp["errmsg"])
        return resp["file_list"][0]["download_url"]
    except Exception as e:
        print("获取下载链接失败")
        raise e

汇总

各个流程编写完成后简单汇总一下

PYTHON  
1
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
def wx_upload_file(config, filepath, uploadpath):
    """
    上传文件
    args:
        config: 配置
        filepath: 文件的路径
        uploadpath: 上传的路径,不能以/开头,以/结尾
    return: url
    """
    realpath = os.path.realpath(filepath)
    filename = os.path.basename(realpath)
    with open(realpath, "rb") as f:
        data = f.read()
    # 获取access_token
    token = get_access_token(config["appid"], config["secret"], config)
    # 获取上传文件链接
    info = get_upload_file_info(token, config["env"], uploadpath + filename)
    upload_file(
        info["url"],
        uploadpath + filename,
        info["authorization"],
        info["token"],
        info["cos_file_id"],
        data,
    )
    download_url = get_download_link(token, config["env"], info['file_id'])
    return (download_url, info['file_id'])

完整代码

PYTHON  
1
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
import requests
import json
import time
import os


def get_access_token(appid: str, secret: str, config) -> str:
    """
    获取调用凭证
    https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/access-token/auth.getAccessToken.html
    args:
        apppid: 小程序appid
        secret: 小程序appSecret

    return: access_token
    """
    # 检查本地token
    token = config["access_token"]
    try:
        now = int(time.time())
        if None == token or (now - config["create_time"]) > config["expires_in"]:
            resp = requests.get(
                "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}".format(
                    appid, secret
                )
            )
            resp = resp.json()

            if None == resp["access_token"]:
                raise Exception(resp["errmsg"])
            # 缓存处理
            config["access_token"] = resp["access_token"]
            config["create_time"] = int(time.time())
            config["expires_in"] = resp["expires_in"]

        return config["access_token"]
    except Exception as e:
        print("获取access token失败")
        raise e


def get_upload_file_info(access_token: str, env: str, path: str):
    """
    上传文件信息
    https://developers.weixin.qq.com/miniprogram/dev/wxcloudrun/src/development/storage/service/upload.html
    args:
        access_token: 
        env: 云托管的路径
        path: 存放位置
    return:
        json: {
            errcode,
            errmsg,
            url,
            token,
            authorization,
            file_id,
            cos_file_id
        }
    """
    try:
        url = "https://api.weixin.qq.com/tcb/uploadfile?access_token={0}".format(
            access_token
        )
        headers = {"Content-Type": "application/json;charset=UTF-8"}
        data = {"env": env, "path": path}
        resp = requests.post(url, json=data, headers=headers)
        # 转json
        resp = resp.json()
        if 0 != resp["errcode"]:
            print(resp["errmsg"])
            print(resp)
            raise Exception(resp["errmsg"])
        # 返回上传文件信息
        return resp
    except Exception as e:
        print("获取文件上传链接失败")
        raise e


def upload_file(url, request_path, authorization, token, file_id, file):
    """
    上传文件
    https://developers.weixin.qq.com/miniprogram/dev/wxcloudrun/src/development/storage/service/upload.html
    args:
        url: 上传图片路径 由get_upload获得的url
        request_path: 上传路径,和get_upload_file_info内部的一致
        authorization: info返回的token
        file_id: cos_file_id
        file: 文件的二进制数据
    return: None
    """
    try:
        form = {
            "key": request_path,
            "Signature": authorization,
            "x-cos-security-token": token,
            "x-cos-meta-fileid": file_id,
            "file": file,
        }
        resp = requests.post(url=url, files=form)
        return resp
    except Exception as e:
        print("上传文件失败")
        raise e


def get_download_link(access_token, env, file_id):
    """
    获取文件下载链接
    https://developers.weixin.qq.com/miniprogram/dev/wxcloudrun/src/development/storage/service/download.html
    args:
        access_token: 调用token
        env: 云环境env
        file_id: file_id
    return: 文件下载链接
    """
    try:
        url = "https://api.weixin.qq.com/tcb/batchdownloadfile?access_token={0}".format(
            access_token
        )
        #headers = {"Content-Type": "application/json"}
        data = {
            "env": env,
            "file_list": [
                {
                    "fileid": file_id
                    # max_age
                }
            ],
        }
        resp = requests.post(url, json=data)
        resp = resp.json()
        if 0 != resp["errcode"]:
            print(resp["errmsg"])
            raise Exception(resp["errmsg"])
        return resp["file_list"][0]["download_url"]
    except Exception as e:
        print("获取下载链接失败")
        raise e


def load_config(path="./config.json"):
    """
    获取配置,主要是读取缓存的access_token
    args: 读取路径
    return: config
    """
    with open(path, "r") as f:
        config = json.load(f)
    return config


def save_config(config, path="./config.json"):
    """
    保持配置
    args: 读取路径
    """
    with open(path, "w") as f:
        json.dump(config, f, indent=4)


def wx_upload_file(config, filepath, uploadpath):
    """
    上传文件
    args:
        config: 配置
        filepath: 文件的路径
        uploadpath: 上传的路径,不能以/开头,以/结尾
    return: url
    """
    realpath = os.path.realpath(filepath)
    filename = os.path.basename(realpath)
    with open(realpath, "rb") as f:
        data = f.read()
    # 获取access_token
    token = get_access_token(config["appid"], config["secret"], config)
    # 获取上传文件链接
    info = get_upload_file_info(token, config["env"], uploadpath + filename)
    upload_file(
        info["url"],
        uploadpath + filename,
        info["authorization"],
        info["token"],
        info["cos_file_id"],
        data,
    )
    download_url = get_download_link(token, config["env"], info['file_id'])
    return (download_url, info['file_id'])


if __name__ == "__main__":
    config = load_config("./config.json")
    #print(wx_upload_file(config, "121.jpg", "temp3/"))

    # 有url
    # print(wx_upload_file(config,'./temp.jpg','temp3/'))
    #
    # # 无url
    path = "./154190339_附件/"
    filenames = os.listdir(path)
    for filename in filenames:
        print(filename)
        image = os.path.join(path, filename)
        url = wx_upload_file(config, image, "temp3/")
        print(url)

    save_config(config, "./config.json")

后记

有一说一,腾讯提供的 cdn 还是比之前自己搭建的要快很多,很舒服,就是花钱很难受。