package sdks import ( "bytes" "crypto/sha256" "fmt" "io" "mime" "mime/multipart" "net/http" "net/url" "strings" "github.com/google/go-querystring/query" "gitlink.org.cn/cloudream/common/consts/errorcode" "gitlink.org.cn/cloudream/common/pkgs/iterator" "gitlink.org.cn/cloudream/common/utils/http2" "gitlink.org.cn/cloudream/common/utils/io2" "gitlink.org.cn/cloudream/common/utils/serder" ) type CodeMessageError struct { Code string Message string } func (e *CodeMessageError) Error() string { return fmt.Sprintf("code: %s, message: %s", e.Code, e.Message) } type RequestParam struct { Method string Path string Query url.Values Header http.Header Body RequestBody } func (p *RequestParam) MakeRequest(baseURL string) (*http.Request, error) { var body io.ReadCloser bodyLen := int64(0) if p.Body != nil { body = p.Body.IntoStream() bodyLen = p.Body.Length() } req, err := http.NewRequest(p.Method, joinUrlUnsafe(baseURL, p.Path), body) if err != nil { return nil, err } req.ContentLength = bodyLen req.URL.RawQuery = p.Query.Encode() if p.Header != nil { req.Header = p.Header } return req, nil } func (p *RequestParam) Do(baseURL string, cli *http.Client) (*http.Response, error) { req, err := p.MakeRequest(baseURL) if err != nil { return nil, err } return cli.Do(req) } func joinUrlUnsafe(base string, path string) string { if strings.HasSuffix(base, "/") { if strings.HasPrefix(path, "/") { return base + path[1:] } return base + path } if strings.HasPrefix(path, "/") { return base + path } return base + "/" + path } type RequestBody interface { // 请求体长度,如果长度未知,返回-1 Length() int64 // 将内部值变成一个流,用于发送请求 IntoStream() io.ReadCloser // 计算Sha256哈希,如果不方便计算,则返回nil Hash() []byte } type StringBody struct { Value string } func (s *StringBody) Length() int64 { return int64(len(s.Value)) } func (s *StringBody) IntoStream() io.ReadCloser { return io.NopCloser(bytes.NewReader([]byte(s.Value))) } func (s *StringBody) Hash() []byte { hash := sha256.Sum256([]byte(s.Value)) return hash[:] } type BytesBody struct { Value []byte } func (b *BytesBody) Length() int64 { return int64(len(b.Value)) } func (b *BytesBody) IntoStream() io.ReadCloser { return io.NopCloser(bytes.NewReader(b.Value)) } func (b *BytesBody) Hash() []byte { hash := sha256.Sum256(b.Value) return hash[:] } type StreamBody struct { Stream io.ReadCloser LengthHint int64 // 长度提示,如果长度未知,可以设置为-1 } func (s *StreamBody) Length() int64 { return s.LengthHint } func (s *StreamBody) IntoStream() io.ReadCloser { return s.Stream } func (s *StreamBody) Hash() []byte { return nil } type APIRequest interface { MakeParam() *RequestParam } func MakeJSONParam(method string, path string, body any) *RequestParam { data, err := serder.ObjectToJSONEx(body) if err != nil { // 开发人员应该保证param是可序列化的 panic(err) } return &RequestParam{ Method: method, Path: path, Body: &BytesBody{Value: data}, } } func MakeQueryParam(method string, path string, q any) *RequestParam { values, err := query.Values(q) if err != nil { // 开发人员应该保证param是可序列化的 panic(err) } return &RequestParam{ Method: method, Path: path, Query: values, } } type UploadFileInfo struct { FileName string File io.ReadCloser } func MakeUploadParam(method string, path string, info any, file UploadFileInfo) *RequestParam { data, err := serder.ObjectToJSONEx(info) if err != nil { // 开发人员应该保证param是可序列化的 panic(err) } pr, pw := io.Pipe() mw := multipart.NewWriter(pw) go func() { defer mw.Close() err := mw.WriteField("info", string(data)) if err != nil { pw.CloseWithError(err) return } fw, err := mw.CreateFormFile("file", file.FileName) if err != nil { pw.CloseWithError(err) return } _, err = io.Copy(fw, file.File) if err != nil { pw.CloseWithError(err) return } }() headers := http.Header{} headers.Set("Content-Type", fmt.Sprintf("%s;boundary=%s", http2.ContentTypeMultiPart, mw.Boundary())) return &RequestParam{ Method: method, Path: path, Header: headers, Body: &StreamBody{ Stream: pr, LengthHint: -1, }, } } type MultiUploadIter = iterator.Iterator[UploadFileInfo] func MakeMultiUploadParam(method string, path string, info any, files MultiUploadIter) *RequestParam { data, err := serder.ObjectToJSONEx(info) if err != nil { // 开发人员应该保证param是可序列化的 panic(err) } pr, pw := io.Pipe() mw := multipart.NewWriter(pw) go func() { defer mw.Close() err := mw.WriteField("info", string(data)) if err != nil { pw.CloseWithError(err) return } for { file, err := files.MoveNext() if err == iterator.ErrNoMoreItem { break } if err != nil { pw.CloseWithError(fmt.Errorf("opening file: %w", err)) return } w, err := mw.CreateFormFile("files", url.PathEscape(file.FileName)) if err != nil { file.File.Close() pw.CloseWithError(fmt.Errorf("create form file failed, err: %w", err)) return } _, err = io.Copy(w, file.File) if err != nil { file.File.Close() pw.CloseWithError(err) return } file.File.Close() } }() headers := http.Header{} headers.Set("Content-Type", fmt.Sprintf("%s;boundary=%s", http2.ContentTypeMultiPart, mw.Boundary())) return &RequestParam{ Method: method, Path: path, Header: headers, Body: &StreamBody{ Stream: pr, LengthHint: -1, }, } } type APIResponse interface { ParseResponse(resp *http.Response) error } type CodeDataResponse[T any] struct { Code string `json:"code"` Message string `json:"message"` Data T `json:"data"` } func (r *CodeDataResponse[T]) Unwarp() (T, error) { if r.Code == errorcode.OK { return r.Data, nil } var def T return def, &CodeMessageError{Code: r.Code, Message: r.Message} } func ParseCodeDataJSONResponse[T any](resp *http.Response, ret T) error { contType := resp.Header.Get("Content-Type") if strings.Contains(contType, http2.ContentTypeJSON) { var err error var r CodeDataResponse[T] r.Data = ret if err = serder.JSONToObjectStreamExRaw(resp.Body, &r); err != nil { return fmt.Errorf("parsing response: %w", err) } if r.Code != errorcode.OK { return &CodeMessageError{Code: r.Code, Message: r.Message} } return nil } cont, err := io2.ReadMost(resp.Body, 200) if err != nil { return fmt.Errorf("unknow response content type: %s, status: %d", contType, resp.StatusCode) } strCont := string(cont) return fmt.Errorf("unknow response content type: %s, status: %d, body[:200]: %s", contType, resp.StatusCode, strCont) } func ParseFileResponse(resp *http.Response) (string, io.ReadCloser, error) { contType := resp.Header.Get("Content-Type") if strings.Contains(contType, http2.ContentTypeJSON) { var err error var r CodeDataResponse[any] if err = serder.JSONToObjectStreamExRaw(resp.Body, &r); err != nil { return "", nil, fmt.Errorf("parsing response: %w", err) } return "", nil, &CodeMessageError{Code: r.Code, Message: r.Message} } _, params, err := mime.ParseMediaType(resp.Header.Get("Content-Disposition")) if err != nil { return "", nil, fmt.Errorf("parsing content disposition: %w", err) } return params["filename"], resp.Body, nil } func IsErrorCode(err error, code string) bool { if e, ok := err.(*CodeMessageError); ok { return e.Code == code } return false }