Skip to content

Commit

Permalink
feat(cloudRecording):implement cloudRecording rest api
Browse files Browse the repository at this point in the history
  • Loading branch information
seymourtang committed Dec 29, 2023
1 parent 535fa14 commit a756fc6
Show file tree
Hide file tree
Showing 24 changed files with 2,862 additions and 4 deletions.
9 changes: 5 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
Expand All @@ -15,7 +12,11 @@
*.out

# Dependency directories (remove the comment below to include it)
# vendor/
vendor/

# Go workspace file
go.work

.idea
.vscode
.DS_Store
99 changes: 99 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Agora REST Client for Go

`agora-rest-client-go`是用Go语言编写的一个开源项目,专门为 Agora REST API设计。它包含了 Agora 官方提供的REST API接口的包装和内部实现,可以帮助开发者更加方便的集成服务端Agora REST API。
## 特性
* 封装了Agora REST API的请求和响应处理,简化与Agora REST API 的通信流程
* 当遇到 DNS 解析失败、网络错误或者请求超时等问题的时候,提供了自动切换最佳域名的能力,以保障请求 REST API 服务的可用性
* 提供了易于使用的API,可轻松地实现调用 Agora REST API 的常见功能,如开启云录制、停止云录制等
* 基于Go语言,具有高效性、并发性和可扩展性

## 支持的服务
* [云端录制 Cloud Recording ](./services/cloudrecording/README.md)

## 环境准备
* [Go 1.18 或以上版本](https://go.dev/)
* 在声网 [Console 平台](https://console.shengwang.cn/)申请的 App ID 和 App Certificate
* 在声网 [Console 平台](https://console.shengwang.cn/)的 Basic Auth 认证信息
* 在声网 [Console 平台](https://console.shengwang.cn/)开启相关的服务能力

## 安装
使用以下命令从 GitHub 安装依赖:
```shell
go get -u github.com/AgoraIO/agora-rest-client-go
```
## 使用示例
以调用云录制服务为例:
```go
package main

import (
"context"
"log"

"github.com/AgoraIO/agora-rest-client-go/core"
"github.com/AgoraIO/agora-rest-client-go/services/cloudrecording"
v1 "github.com/AgoraIO/agora-rest-client-go/services/cloudrecording/v1"
)

const (
appId = "<your appId>"
username = "<the username of basic auth credential>"
password = "<the password of basic auth credential>"
cname = "<your cname>"
uid = "<your uid>"
)

func main() {
// 初始化Agora REST API客户端
client := core.NewClient(&core.Config{
AppID: appId,
Credential: core.NewBasicAuthCredential(username, password),
// 指定服务器所在的区域,可选值有CN, NA, EU, AP,client 将会根据配置的区域自动切换使用最佳的域名
RegionCode: core.CN,
// 指定日志输出的级别,可选值有LogDebug, LogInfo, LogWarn, LogError
// 如果要关闭日志输出,可将 logger 设置为 DiscardLogger
Logger: core.NewDefaultLogger(core.LogDebug),
})
// 初始化云端录制服务 API
cloudRecordingAPI := cloudrecording.NewAPI(client)

// 调用云端录制服务 API 的Acquire接口
resp, err := cloudRecordingAPI.V1().Acquire().Do(context.TODO(), &v1.AcquirerReqBody{
Cname: cname,
Uid: uid,
ClientRequest: &v1.AcquirerClientRequest{
Scene: 0,
ResourceExpiredHour: 24,
},
})
// 处理非业务错误
if err != nil {
log.Fatal(err)
}
if resp.IsSuccess() {
// 处理业务上成功响应
log.Printf("resourceId:%s", resp.SuccessRes.ResourceId)
} else {
// 处理业务上失败响应
log.Printf("resp:%+v", resp)
}
}
```
更多的示例可在[Example](./examples) 查看

## 贡献
本项目欢迎并接受贡献。如果您在使用中遇到问题或有改进建议,请提出issue或向我们提交Pull Request。

# SemVer 版本规范
本项目使用语义化版本号规范 (SemVer) 来管理版本。格式为 MAJOR.MINOR.PATCH。

* MAJOR 版本号表示不向后兼容的重大更改。
* MINOR 版本号表示向后兼容的新功能或增强。
* PATCH 版本号表示向后兼容的错误修复和维护。
有关详细信息,请参阅 [语义化版本](https://semver.org/lang/zh-CN/) 规范。

## 参考
* [Agora API 文档](https://doc.shengwang.cn/)

## 许可证
该项目使用MIT许可证,详细信息请参阅LICENSE文件。
219 changes: 219 additions & 0 deletions core/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
package core

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)

type Client interface {
GetAppID() string
DoREST(ctx context.Context, path string, method string, requestBody interface{}) (*BaseResponse, error)
}

type Config struct {
AppID string
Timeout time.Duration
Credential Credential

RegionCode RegionArea
Logger Logger
}

type ClientImpl struct {
appID string
httpClient *http.Client
timeout time.Duration
logger Logger
credential Credential

module string
domainPool *DomainPool
}

var _ Client = (*ClientImpl)(nil)

const defaultTimeout = 10 * time.Second

func NewClient(config *Config) *ClientImpl {
if config.Timeout == 0 {
config.Timeout = defaultTimeout
}
cc := &http.Client{
Timeout: config.Timeout,
}
if config.Logger == nil {
config.Logger = defaultLogger
}
return &ClientImpl{
appID: config.AppID,
credential: config.Credential,
httpClient: cc,
timeout: config.Timeout,
logger: config.Logger,
module: "http client",
domainPool: NewDomainPool(config.RegionCode, config.Logger),
}
}

func (c *ClientImpl) marshalBody(body interface{}) (io.Reader, error) {
if body == nil {
return nil, nil
}
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, err
}
c.logger.Debugf(context.Background(), c.module, "http request body:%s", jsonBody)
return bytes.NewReader(jsonBody), nil
}

func (c *ClientImpl) DoREST(ctx context.Context, path string,
method string, requestBody interface{}) (*BaseResponse, error) {
timeoutCtx, cancelFunc := context.WithTimeout(ctx, c.timeout)
defer func() {
_ = cancelFunc
}()

var (
resp *BaseResponse
err error
retry int
)

err = RetryDo(func(retryCount int) error {
var doErr error

resp, doErr = c.doREST(timeoutCtx, path, method, requestBody)
if doErr != nil {
return NewRetryErr(false, doErr)
}

statusCode := resp.HttpStatusCode
switch {
case statusCode >= 200 && statusCode <= 300:
return nil
case statusCode >= 400 && statusCode < 410:
c.logger.Debugf(ctx, c.module, "http status code is %d, no retry,http response:%s", statusCode, resp.RawBody)
return &RetryErr{
false,
NewInternalErr(fmt.Sprintf("http status code is %d, no retry,http response:%s", statusCode, resp.RawBody)),
}
default:
c.logger.Debugf(ctx, c.module, "http status code is %d, retry,http response:%s", statusCode, resp.RawBody)
return fmt.Errorf("http status code is %d, retry", resp.RawBody)
}
}, func() bool {
select {
case <-ctx.Done():
return true
default:
}
return retry >= 3
}, func(i int) time.Duration {
return time.Second * time.Duration(i+1)
}, func(err error) {
c.logger.Debugf(ctx, c.module, "http request err:%s", err)
retry++
})
if resp != nil {
c.logger.Debugf(ctx, c.module, "http response:%s", resp.RawBody)
}
return resp, err
}

func (c *ClientImpl) doREST(ctx context.Context, path string,
method string, requestBody interface{}) (*BaseResponse, error) {

var (
err error
resp *http.Response
req *http.Request
)

if err = c.domainPool.SelectBestDomain(ctx); err != nil {
return nil, err
}

doHttpRequest := func() error {
req, err = c.createRequest(ctx, path, method, requestBody)
if err != nil {
return err
}
resp, err = c.httpClient.Do(req)
return err
}
err = RetryDo(
func(retryCount int) error {
c.logger.Debugf(ctx, c.module, "http retry attempt:%d", retryCount)
return doHttpRequest()
},
func() bool {
select {
case <-ctx.Done():
return true
default:
}
return false
},
func(i int) time.Duration {
if i == 0 {
return 0 * time.Second
}
return 500 * time.Millisecond
},
func(err error) {
c.logger.Debugf(ctx, c.module, "http request err:%s", err)
c.domainPool.NextRegion()
},
)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

return &BaseResponse{
RawBody: body,
HttpStatusCode: resp.StatusCode,
}, nil
}

func (c *ClientImpl) addCredential(req *http.Request) {
if c.credential != nil {
c.credential.Visit(req)
}
}

func (c *ClientImpl) GetAppID() string {
return c.appID
}

func (c *ClientImpl) createRequest(ctx context.Context, path string,
method string, requestBody interface{}) (*http.Request, error) {
url := c.domainPool.GetCurrentUrl() + path

c.logger.Debugf(ctx, "create request url:%s", url)
jsonBody, err := c.marshalBody(requestBody)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, method, url, jsonBody)
if err != nil {
return nil, err
}
c.addCredential(req)
req.Header.Set("Content-Type", "application/json;charset=utf-8")

return req, nil
}
36 changes: 36 additions & 0 deletions core/credential.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package core

import (
"net/http"
)

type CredentialType int

const (
BasicAuth CredentialType = iota
)

type Credential interface {
Type() CredentialType
Visit(r *http.Request)
}

type BasicAuthCredential struct {
Username string
Password string
}

func NewBasicAuthCredential(username string, password string) *BasicAuthCredential {
return &BasicAuthCredential{
Username: username,
Password: password,
}
}

func (b *BasicAuthCredential) Type() CredentialType {
return BasicAuth
}

func (b *BasicAuthCredential) Visit(r *http.Request) {
r.SetBasicAuth(b.Username, b.Password)
}
Loading

0 comments on commit a756fc6

Please sign in to comment.