Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 添加大文件上传 #555

Merged
merged 1 commit into from
Dec 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 63 additions & 5 deletions caller.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package openwechat
import (
"bytes"
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"encoding/xml"
"errors"
Expand Down Expand Up @@ -325,12 +327,63 @@ type CallerUploadMediaOptions struct {

func (c *Caller) UploadMedia(ctx context.Context, file *os.File, opt *CallerUploadMediaOptions) (*UploadResponse, error) {
// 首先尝试上传图片
h := md5.New()
if _, err := io.Copy(h, file); err != nil {
return nil, err
}
fileMd5 := hex.EncodeToString(h.Sum(nil))
stat, err := file.Stat()
if err != nil {
return nil, err
}
filename := file.Name()
filesize := stat.Size()

clientWebWxUploadMediaByChunkOpt := &ClientWebWxUploadMediaByChunkOptions{
FromUserName: opt.FromUserName,
ToUserName: opt.ToUserName,
BaseRequest: opt.BaseRequest,
LoginInfo: opt.LoginInfo,
FromUserName: opt.FromUserName,
ToUserName: opt.ToUserName,
BaseRequest: opt.BaseRequest,
LoginInfo: opt.LoginInfo,
Filename: filename,
FileMD5: fileMd5,
FileSize: filesize,
LastModifiedDate: stat.ModTime(),
}

if filesize > needCheckSize {
checkUploadRequest := webWxCheckUploadRequest{
BaseRequest: opt.BaseRequest,
FileMd5: fileMd5,
FileName: filename,
FileSize: filesize,
FileType: 7,
FromUserName: opt.FromUserName,
ToUserName: opt.ToUserName,
}
resp, err := c.Client.webWxCheckUploadRequest(ctx, checkUploadRequest)
if err != nil {
return nil, err
}
defer func() { _ = resp.Body.Close() }()
var checkUploadResponse webWxCheckUploadResponse
if err = json.NewDecoder(resp.Body).Decode(&checkUploadResponse); err != nil {
return nil, err
}
if err = checkUploadResponse.BaseResponse.Err(); err != nil {
return nil, err
}
// 如果已经上传过了,直接返回
if checkUploadResponse.MediaId != "" {
var item UploadResponse
item.MediaId = checkUploadResponse.MediaId
item.Signature = checkUploadResponse.Signature
item.BaseResponse = checkUploadResponse.BaseResponse
return &item, nil
}
clientWebWxUploadMediaByChunkOpt.AESKey = checkUploadResponse.AESKey
clientWebWxUploadMediaByChunkOpt.Signature = checkUploadResponse.Signature
}

resp, err := c.Client.WebWxUploadMediaByChunk(ctx, file, clientWebWxUploadMediaByChunkOpt)
// 无错误上传成功之后获取请求结果,判断结果是否正常
if err != nil {
Expand All @@ -347,6 +400,7 @@ func (c *Caller) UploadMedia(ctx context.Context, file *os.File, opt *CallerUplo
if len(item.MediaId) == 0 {
return &item, errors.New("upload failed")
}
item.Signature = clientWebWxUploadMediaByChunkOpt.Signature
return &item, nil
}

Expand Down Expand Up @@ -418,13 +472,17 @@ func (c *Caller) WebWxSendFile(ctx context.Context, reader io.Reader, opt *Calle
return nil, err
}
// 构造新的文件类型的信息
stat, _ := file.Stat()
stat, err := file.Stat()
if err != nil {
return nil, err
}
appMsg := newFileAppMessage(stat, resp.MediaId)
content, err := appMsg.XmlByte()
if err != nil {
return nil, err
}
msg := NewSendMessage(AppMessage, string(content), opt.FromUserName, opt.ToUserName, "")
msg.Signature = resp.Signature
return c.WebWxSendAppMsg(ctx, msg, opt.BaseRequest)
}

Expand Down
125 changes: 88 additions & 37 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package openwechat
import (
"bytes"
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -479,37 +477,87 @@ func (c *Client) WebWxGetHeadImg(ctx context.Context, user *User) (*http.Respons
return c.Do(req)
}

type ClientWebWxUploadMediaByChunkOptions struct {
FromUserName string
ToUserName string
BaseRequest *BaseRequest
LoginInfo *LoginInfo
type webWxCheckUploadRequest struct {
BaseRequest *BaseRequest `json:"BaseRequest"`
FileMd5 string `json:"FileMd5"`
FileName string `json:"FileName"`
FileSize int64 `json:"FileSize"`
FileType uint8 `json:"FileType"`
FromUserName string `json:"FromUserName"`
ToUserName string `json:"ToUserName"`
}

// WebWxUploadMediaByChunk 分块上传文件
// TODO 优化掉这个函数
func (c *Client) WebWxUploadMediaByChunk(ctx context.Context, file *os.File, opt *ClientWebWxUploadMediaByChunkOptions) (*http.Response, error) {
// 获取文件上传的类型
contentType, err := GetFileContentType(file)
type webWxCheckUploadResponse struct {
BaseResponse BaseResponse `json:"BaseResponse"`
MediaId string `json:"MediaId"`
AESKey string `json:"AESKey"`
Signature string `json:"Signature"`
EntryFileName string `json:"EncryFileName"`
}

func (c *Client) webWxCheckUploadRequest(ctx context.Context, req webWxCheckUploadRequest) (*http.Response, error) {
path, err := url.Parse(c.Domain.BaseHost() + webwxcheckupload)
if err != nil {
return nil, err
}
if _, err = file.Seek(0, io.SeekStart); err != nil {
body, err := jsonEncode(req)
if err != nil {
return nil, err
}

// 获取文件的md5
h := md5.New()
if _, err = io.Copy(h, file); err != nil {
reqs, err := http.NewRequestWithContext(ctx, http.MethodPost, path.String(), body)
if err != nil {
return nil, err
}
fileMd5 := hex.EncodeToString(h.Sum(nil))
reqs.Header.Add("Content-Type", jsonContentType)
return c.Do(reqs)
}

type uploadMediaRequest struct {
UploadType uint8 `json:"UploadType"`
BaseRequest *BaseRequest `json:"BaseRequest"`
ClientMediaId int64 `json:"ClientMediaId"`
TotalLen int64 `json:"TotalLen"`
StartPos int `json:"StartPos"`
DataLen int64 `json:"DataLen"`
MediaType uint8 `json:"MediaType"`
FromUserName string `json:"FromUserName"`
ToUserName string `json:"ToUserName"`
FileMd5 string `json:"FileMd5"`
AESKey string `json:"AESKey,omitempty"`
Signature string `json:"Signature,omitempty"`
}

type ClientWebWxUploadMediaByChunkOptions struct {
FromUserName string
ToUserName string
BaseRequest *BaseRequest
LoginInfo *LoginInfo
Filename string
FileMD5 string
FileSize int64
LastModifiedDate time.Time
AESKey string
Signature string
}

sate, err := file.Stat()
type UploadFile interface {
io.ReaderAt
io.ReadSeeker
}

// WebWxUploadMediaByChunk 分块上传文件
// TODO 优化掉这个函数
func (c *Client) WebWxUploadMediaByChunk(ctx context.Context, file UploadFile, opt *ClientWebWxUploadMediaByChunkOptions) (*http.Response, error) {
// 获取文件上传的类型
if _, err := file.Seek(0, io.SeekStart); err != nil {
return nil, err
}
contentType, err := GetFileContentType(file)
if err != nil {
return nil, err
}
filename := sate.Name()

filename := opt.Filename

if ext := filepath.Ext(filename); ext == "" {
names := strings.Split(contentType, "/")
Expand All @@ -530,22 +578,24 @@ func (c *Client) WebWxUploadMediaByChunk(ctx context.Context, file *os.File, opt

cookies := c.Jar().Cookies(path)

webWxDataTicket, err := getWebWxDataTicket(cookies)
webWxDataTicket, err := wxDataTicket(cookies)
if err != nil {
return nil, err
}

uploadMediaRequest := map[string]interface{}{
"UploadType": 2,
"BaseRequest": opt.BaseRequest,
"ClientMediaId": time.Now().Unix() * 1e4,
"TotalLen": sate.Size(),
"StartPos": 0,
"DataLen": sate.Size(),
"MediaType": 4,
"FromUserName": opt.FromUserName,
"ToUserName": opt.ToUserName,
"FileMd5": fileMd5,
uploadMediaRequest := &uploadMediaRequest{
UploadType: 2,
BaseRequest: opt.BaseRequest,
ClientMediaId: time.Now().Unix() * 1e4,
TotalLen: opt.FileSize,
StartPos: 0,
DataLen: opt.FileSize,
MediaType: 4,
FromUserName: opt.FromUserName,
ToUserName: opt.ToUserName,
FileMd5: opt.FileMD5,
AESKey: opt.AESKey,
Signature: opt.Signature,
}

uploadMediaRequestByte, err := json.Marshal(uploadMediaRequest)
Expand All @@ -554,14 +604,14 @@ func (c *Client) WebWxUploadMediaByChunk(ctx context.Context, file *os.File, opt
}

// 计算上传文件的次数
chunks := int((sate.Size() + chunkSize - 1) / chunkSize)
chunks := int((opt.FileSize + chunkSize - 1) / chunkSize)

content := map[string]string{
"id": "WU_FILE_0",
"name": filename,
"type": contentType,
"lastModifiedDate": sate.ModTime().Format(TimeFormat),
"size": strconv.FormatInt(sate.Size(), 10),
"lastModifiedDate": opt.LastModifiedDate.Format(TimeFormat),
"size": strconv.FormatInt(opt.FileSize, 10),
"mediatype": mediaType,
"webwx_data_ticket": webWxDataTicket,
"pass_ticket": opt.LoginInfo.PassTicket,
Expand Down Expand Up @@ -596,7 +646,7 @@ func (c *Client) WebWxUploadMediaByChunk(ctx context.Context, file *os.File, opt
}

// create form file
fileWriter, err := writer.CreateFormFile("filename", file.Name())
fileWriter, err := writer.CreateFormFile("filename", filename)
if err != nil {
return err
}
Expand Down Expand Up @@ -671,6 +721,7 @@ func (c *Client) WebWxSendAppMsg(ctx context.Context, msg *SendMessage, request
params := url.Values{}
params.Add("fun", "async")
params.Add("f", "json")
params.Add("lang", "zh_CN")
path.RawQuery = params.Encode()
return c.sendMessage(ctx, request, path.String(), msg)
}
Expand Down Expand Up @@ -815,7 +866,7 @@ func (c *Client) WebWxGetMedia(ctx context.Context, msg *Message, info *LoginInf
return nil, err
}
cookies := c.Jar().Cookies(path)
webWxDataTicket, err := getWebWxDataTicket(cookies)
webWxDataTicket, err := wxDataTicket(cookies)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion cookiejar.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func (c CookieGroup) GetByName(cookieName string) (cookie *http.Cookie, exist bo
return nil, false
}

func getWebWxDataTicket(cookies []*http.Cookie) (string, error) {
func wxDataTicket(cookies []*http.Cookie) (string, error) {
cookieGroup := CookieGroup(cookies)
cookie, exist := cookieGroup.GetByName("webwx_data_ticket")
if !exist {
Expand Down
5 changes: 3 additions & 2 deletions entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,9 @@ type MessageResponse struct {
}

type UploadResponse struct {
BaseResponse BaseResponse
MediaId string
BaseResponse BaseResponse `json:"BaseResponse"`
MediaId string `json:"MediaId"`
Signature string `json:"Signature"`
}

type PushLoginResponse struct {
Expand Down
1 change: 1 addition & 0 deletions message.go
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@ type SendMessage struct {
MediaId string `json:"MediaId,omitempty"`
EmojiFlag int `json:"EmojiFlag,omitempty"`
EMoticonMd5 string `json:"EMoticonMd5,omitempty"`
Signature string `json:"Signature,omitempty"`
}

// NewSendMessage SendMessage的构造方法
Expand Down
Loading