-
微信
-
支付宝
# 聊一聊乐变上传自动化接入
## 目标 & 背景
最近我司从外部搞了一个项目,技术栈非常杂,客户端业务为 C# + tolua,打包是 python,服务端是 go,后台是 php + js,其中客户端的资源下载接入的乐变
由于项目有众多渠道,平均一个包的包体差不多就有2G,日常发版流程非常辛苦,除了下图的过程外,如果是包更,还需要运营同学手动上传到各大 store,再加上这个项目本身的打包流程设计功底较差,平均一个渠道出包的时间为 1 小时左右
这样就导致最糟情况下,全量发一个版本的时间成本要 1 天,非常难以置信
![](https://blog.liuocean.synology.me:9001/blog/old/17057147615938.jpg)
本片文章会主要围绕乐变的文件上传相关过程碰到的坑,其他部分不会详细介绍,其中主要使用如下开源库
- [CliToolkit](https://github.com/LiuOcean/CliToolkit) 之前开源的 Cli 框架
- [RestSharp](https://github.com/restsharp/RestSharp) http 客户端
- [Masuit.Tools](https://github.com/ldqk/Masuit.Tools) 处理 rsa 等加密内容
## 请求加密过程
乐变提供了相关接入过程的 pyhton demo,正常需要发起 login 请求,获取当前账号的 rsa 公钥,但是乐变默认返回的公钥尾部多了一个换行,这样会导致在 C# 中,无论怎么加密都有问题
在使用时,记得 TrimEnd('\n')
```
-----BEGIN PUBLIC KEY-----
xxx
END PUBLIC KEY-----
```
接着以 **uploadfile.php** 接口为例,最开始看到 demo 代码中的 encData 的数据结构,我以为在 data.encdata 中存的是 string 类型,实际上 encData 在调用 rsa_long_encrypt 后,就变成了 string[],在检查 C# 代码到底哪里不一样,着实花了不少时间
```python
encData = {
"chid": chid,
"status": "chunksMerge",
"name": name,
"part": part,
"chunks": totalChunks,
"md5": fileMd5,
"asynchronous" : 0,#是否是同步合并,1为异步,需轮询合并状态
"nosdk": '1'#0表示有sdk,1表示没有sdk
}
encData = json.dumps(encData).encode("utf-8")
encData = base64.b64encode(encData)
encData = rsa_long_encrypt(public_key, encData)
data = {
"chid": chid,
"encdata": encData
}
def rsa_long_encrypt(public_key, msg, length=53):
keyPub = RSA.importKey(public_key)
pubobj = Cipher_pkcs1_v1_5.new(keyPub)
res = []
for i in range(0, len(msg), length):
encryptedStr = pubobj.encrypt(msg[i:i + length])
encryptedStr = base64.b64encode(encryptedStr).decode('utf-8')
res.append(encryptedStr)
return res
```
加密过程翻译成 C# 后,大概如下,encData 需要先转成 json,然后转为 base64,接着使用 Rsa 进行加密,以 53 的长度分割成 List,并存入 Request encdata 中,在发送时,将 Request 转为 base64 的 json 数据
```csharp
public interface ILebianRSA
{
public static virtual List ToRsa(ILebianRSA data)
{
var json = JsonConvert.SerializeObject(data);
return LebianUtils.RsaEncrypt(json.Base64Encrypt());
}
}
public class LebianRequest where T : ILebianRSA
{
[JsonProperty("chid")] public int ch_id { get; private set; }
[JsonProperty("encdata")] public List enc_data { get; private set; }
public LebianRequest(int ch_id, T data)
{
this.ch_id = ch_id;
enc_data = T.ToRsa(data, type);
}
public string ToBase64()
{
var json = JsonConvert.SerializeObject(this);
return json.Base64Encrypt();
}
}
public static partial class LebianUtils
{
public static List RsaEncrypt(string base64, int interval = 53)
{
var key = GetKey(); // 获取当前渠道的公钥
var result = new List();
for(var i = 0; i < base64.Length; i += interval)
{
var sub = base64.Substring(i, Math.Min(interval, base64.Length - i));
result.Add(sub.RSAEncrypt(key));
}
return result;
}
}
```
面对这个接入过程,我很难不吐槽一下,为何乐变的后台域名不是用 ssl 进行加密?反而需要自行实现 ssl 的加密过程
## 文件上传
这里 http 的库之所以选择 RestSharp,很大程度上是为了给上传增加进度条,由于客户端比较大,考虑到网络波动等问题,此处介绍的是分片上传,下图中的 chunk 代表当前上传的是哪一个分段
> 图中 eta 这么短,是因为 chunk 已经上传了,此图仅是示意
![](https://blog.liuocean.synology.me:9001/blog/old/17057197639543.jpg)
在 **RestRequest** 的 **AddFile** 接口中,提供了 **Func getFile** 的参数,这样我们做进度条就简单多了,自定义一个 Stream,在读时,通过外部传入的 ProgressTask,上报读了多少 byte
```csharp
file class ProgressMS : MemoryStream
{
public ProgressTask? root_progress;
public ProgressTask? chunk_progress;
public override async ValueTask ReadAsync(Memory buffer,
CancellationToken cancellationToken = new CancellationToken())
{
var result = await base.ReadAsync(buffer, cancellationToken);
root_progress?.Increment(buffer.Length);
chunk_progress?.Increment(buffer.Length);
return result;
}
}
```
我在接入 uploadfile.php 接口时,碰到了两个主要问题
- 为什么二次上传 chunk 依然全量上传
- 为什么上传后的文件提示文件格式非法
首先说第一个问题,正常我们需要先问当前 chunk 是否需要上传,然后根据返回值选择是否需要 upload,我发现官方提供的 python demo 这个功能是好的,而我写的 C# 却有问题,我花了很多时间对比两者的差异,最后发现如下区别
```python
# 此处为检查 chunk 是否存在
encData = {
# 略...
"status":"chunkCheck",
"chunkIndex":chunk_index
}
# 此处为上传 chunk
encData = {
# 略...
"status":"",
"chunk":chunk_index
}
```
在乐变的后台封装中,chunk 是否存在、上传以及分片的合并,统一都是走的 **uploadfile.php** 接口,通过 **status** 中具体的值进行区分,但是!不同的逻辑使用的结构不一致,同样都是 chunk_index 字段,检查接口要序列化为 **chunkIndex**,上传时需要序列化为 **chunk**
> 这个同样是让我感到困惑的地方
接着我就碰到了第二个问题,为什么上传文件提示文件格式非法?在 RestSharp 中,我使用如下 API 传输一个 chunk
```csharp
request.AddFile("file", () => ms, file.Name);
```
在 python demo 中,是这么上传的
```python
fileData = {'file': (filename, file_chunk_data, 'application/octet-stream')}
```
这就让我非常费解了,大家都是直接把文件名传进去,是什么导致了结果不一致,接着我尝试直接固定文件名为 **test.apk**,就成功了
```csharp
// 成功
request.AddFile("file", () => ms, "test.apk");
// 失败
request.AddFile("file", () => ms, "中文test.apk");
```
这个项目最终出包的文件名是包含中文的,这就可以得出结论,python 和 RestSharp 在面对中文文件时,在处理文件名的过程上存在差异,为了证明这个差异,在 Surge 中直接开启 **流量捕获**,在请求数据的 RAW 一栏,我们可以清楚的看到 RestSharp 发送的文件名是经过转码的
![](https://blog.liuocean.synology.me:9001/blog/old/17057219427050.jpg)
在 python 中面对中文名的文件上传,会将中文转换为 **..**,比如上面的 **中文test.apk**就会被转义成 **....test.apk**,不过好在 RestSharp 也提供了不转义的选项
```csharp
var options = new FileParameterOptions {DisableFilenameEncoding = true};
request.AddFile("file", () => ms, file.Name, ContentType.Binary, options);
```
这样最终的文件名会被转义为 **??test.apk**,所以这个问题的核心是乐变的后台并没有对中文名的文件上传做 utf8 相关的支持,又或者他们 php 后台的标准和 RestSharp 的标准不一致
## 最后
这个项目是其他公司积累了很多年的框架,中间据说换了好几波人,很多代码极难维护。这次乐变的自动化 API 接入过程,让我感到非常难受
后面这个项目,有时间我一定要把乐变剔掉