| @@ -1042,3 +1042,8 @@ RETRY_BACKOFF = 3 | |||
| BROKER = redis://localhost:6379 | |||
| DEFAULT_QUEUE = DecompressTasksQueue | |||
| RESULT_BACKEND = redis://localhost:6379 | |||
| [cloudbrain] | |||
| HOST = http://192.168.204.24 | |||
| USERNAME = | |||
| PASSWORD = | |||
| @@ -44,6 +44,7 @@ require ( | |||
| github.com/go-ini/ini v1.56.0 // indirect | |||
| github.com/go-openapi/jsonreference v0.19.3 // indirect | |||
| github.com/go-redis/redis v6.15.2+incompatible | |||
| github.com/go-resty/resty/v2 v2.3.0 | |||
| github.com/go-sql-driver/mysql v1.4.1 | |||
| github.com/go-swagger/go-swagger v0.21.0 | |||
| github.com/gobwas/glob v0.2.3 | |||
| @@ -107,7 +108,7 @@ require ( | |||
| github.com/yuin/goldmark-meta v0.0.0-20191126180153-f0638e958b60 | |||
| golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 | |||
| golang.org/x/mod v0.3.0 // indirect | |||
| golang.org/x/net v0.0.0-20200506145744-7e3656a0809f | |||
| golang.org/x/net v0.0.0-20200513185701-a91f0712d120 | |||
| golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d | |||
| golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f | |||
| golang.org/x/text v0.3.2 | |||
| @@ -290,6 +290,9 @@ github.com/go-openapi/validate v0.19.3 h1:PAH/2DylwWcIU1s0Y7k3yNmeAgWOcKrNE2Q7Ww | |||
| github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo= | |||
| github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4= | |||
| github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= | |||
| github.com/go-resty/resty v1.12.0 h1:L1P5qymrXL5H/doXe2pKUr1wxovAI5ilm2LdVLbwThc= | |||
| github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So= | |||
| github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU= | |||
| github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= | |||
| github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= | |||
| github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= | |||
| @@ -841,6 +844,8 @@ golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3ob | |||
| golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | |||
| golang.org/x/net v0.0.0-20200506145744-7e3656a0809f h1:QBjCr1Fz5kw158VqdE9JfI9cJnl/ymnJWAdMuinqL7Y= | |||
| golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= | |||
| golang.org/x/net v0.0.0-20200513185701-a91f0712d120 h1:EZ3cVSzKOlJxAd8e8YAJ7no8nNypTxexh/YE/xW3ZEY= | |||
| golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= | |||
| golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | |||
| golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | |||
| golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | |||
| @@ -0,0 +1,271 @@ | |||
| package models | |||
| import ( | |||
| "encoding/json" | |||
| "errors" | |||
| "fmt" | |||
| "time" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| "code.gitea.io/gitea/modules/timeutil" | |||
| "xorm.io/builder" | |||
| ) | |||
| type CloudbrainStatus string | |||
| const ( | |||
| JobWaiting CloudbrainStatus = "WAITING" | |||
| JobStopped CloudbrainStatus = "STOPPED" | |||
| JobSucceeded CloudbrainStatus = "SUCCEEDED" | |||
| JobFailed CloudbrainStatus = "FAILED" | |||
| ) | |||
| type Cloudbrain struct { | |||
| ID int64 `xorm:"pk autoincr"` | |||
| JobID string `xorm:"INDEX NOT NULL"` | |||
| JobName string | |||
| Status string `xorm:"INDEX"` | |||
| UserID int64 `xorm:"INDEX"` | |||
| RepoID int64 `xorm:"INDEX"` | |||
| CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` | |||
| UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` | |||
| User *User `xorm:"-"` | |||
| Repo *Repository `xorm:"-"` | |||
| } | |||
| type CloudBrainLoginResult struct { | |||
| Code string | |||
| Msg string | |||
| Payload map[string]interface{} | |||
| } | |||
| type TaskRole struct { | |||
| Name string `json:"name"` | |||
| TaskNumber int8 `json:"taskNumber"` | |||
| MinSucceededTaskCount int8 `json:"minSucceededTaskCount"` | |||
| MinFailedTaskCount int8 `json:"minFailedTaskCount"` | |||
| CPUNumber int8 `json:"cpuNumber"` | |||
| GPUNumber int8 `json:"gpuNumber"` | |||
| MemoryMB int `json:"memoryMB"` | |||
| ShmMB int `json:"shmMB"` | |||
| Command string `json:"command"` | |||
| NeedIBDevice bool `json:"needIBDevice"` | |||
| IsMainRole bool `json:"isMainRole"` | |||
| } | |||
| type CreateJobParams struct { | |||
| JobName string `json:"jobName"` | |||
| RetryCount int8 `json:"retryCount"` | |||
| GpuType string `json:"gpuType"` | |||
| Image string `json:"image"` | |||
| TaskRoles []TaskRole `json:"taskRoles"` | |||
| } | |||
| type CreateJobResult struct { | |||
| Code string | |||
| Msg string | |||
| Payload map[string]interface{} | |||
| } | |||
| type GetJobResult struct { | |||
| Code string `json:"code"` | |||
| Msg string `json:"msg"` | |||
| Payload map[string]interface{} `json:"payload"` | |||
| } | |||
| type CloudbrainsOptions struct { | |||
| ListOptions | |||
| RepoID int64 // include all repos if empty | |||
| UserID int64 | |||
| JobID int64 | |||
| SortType string | |||
| CloudbrainIDs []int64 | |||
| // JobStatus CloudbrainStatus | |||
| } | |||
| type TaskPod struct { | |||
| TaskRoleStatus struct { | |||
| Name string `json:"name"` | |||
| } `json:"taskRoleStatus"` | |||
| TaskStatuses []struct { | |||
| TaskIndex int `json:"taskIndex"` | |||
| PodUID string `json:"podUid"` | |||
| PodIP string `json:"podIp"` | |||
| PodName string `json:"podName"` | |||
| ContainerID string `json:"containerId"` | |||
| ContainerIP string `json:"containerIp"` | |||
| ContainerGpus string `json:"containerGpus"` | |||
| State string `json:"state"` | |||
| StartAt time.Time `json:"startAt"` | |||
| FinishedAt time.Time `json:"finishedAt"` | |||
| ExitCode int `json:"exitCode"` | |||
| ExitDiagnostics string `json:"exitDiagnostics"` | |||
| RetriedCount int `json:"retriedCount"` | |||
| } `json:"taskStatuses"` | |||
| } | |||
| func ConvertToTaskPod(input map[string]interface{}) (TaskPod, error) { | |||
| data, _ := json.Marshal(input) | |||
| var taskPod TaskPod | |||
| err := json.Unmarshal(data, &taskPod) | |||
| return taskPod, err | |||
| } | |||
| type JobResultPayload struct { | |||
| ID string `json:"id"` | |||
| Name string `json:"name"` | |||
| Platform string `json:"platform"` | |||
| JobStatus struct { | |||
| Username string `json:"username"` | |||
| State string `json:"state"` | |||
| SubState string `json:"subState"` | |||
| ExecutionType string `json:"executionType"` | |||
| Retries int `json:"retries"` | |||
| CreatedTime int64 `json:"createdTime"` | |||
| CompletedTime int64 `json:"completedTime"` | |||
| AppID string `json:"appId"` | |||
| AppProgress string `json:"appProgress"` | |||
| AppTrackingURL string `json:"appTrackingUrl"` | |||
| AppLaunchedTime int64 `json:"appLaunchedTime"` | |||
| AppCompletedTime interface{} `json:"appCompletedTime"` | |||
| AppExitCode int `json:"appExitCode"` | |||
| AppExitDiagnostics string `json:"appExitDiagnostics"` | |||
| AppExitType interface{} `json:"appExitType"` | |||
| VirtualCluster string `json:"virtualCluster"` | |||
| } `json:"jobStatus"` | |||
| TaskRoles map[string]interface{} `json:"taskRoles"` | |||
| Resource struct { | |||
| CPU int `json:"cpu"` | |||
| Memory string `json:"memory"` | |||
| NvidiaComGpu int `json:"nvidia.com/gpu"` | |||
| } `json:"resource"` | |||
| Config struct { | |||
| Image string `json:"image"` | |||
| JobID string `json:"jobId"` | |||
| GpuType string `json:"gpuType"` | |||
| JobName string `json:"jobName"` | |||
| JobType string `json:"jobType"` | |||
| TaskRoles []struct { | |||
| Name string `json:"name"` | |||
| ShmMB int `json:"shmMB"` | |||
| Command string `json:"command"` | |||
| MemoryMB int `json:"memoryMB"` | |||
| CPUNumber int `json:"cpuNumber"` | |||
| GpuNumber int `json:"gpuNumber"` | |||
| IsMainRole bool `json:"isMainRole"` | |||
| TaskNumber int `json:"taskNumber"` | |||
| NeedIBDevice bool `json:"needIBDevice"` | |||
| MinFailedTaskCount int `json:"minFailedTaskCount"` | |||
| MinSucceededTaskCount int `json:"minSucceededTaskCount"` | |||
| } `json:"taskRoles"` | |||
| RetryCount int `json:"retryCount"` | |||
| } `json:"config"` | |||
| Userinfo struct { | |||
| User string `json:"user"` | |||
| OrgID string `json:"org_id"` | |||
| } `json:"userinfo"` | |||
| } | |||
| func ConvertToJobResultPayload(input map[string]interface{}) (JobResultPayload, error) { | |||
| data, _ := json.Marshal(input) | |||
| var jobResultPayload JobResultPayload | |||
| err := json.Unmarshal(data, &jobResultPayload) | |||
| return jobResultPayload, err | |||
| } | |||
| func Cloudbrains(opts *CloudbrainsOptions) ([]*Cloudbrain, int64, error) { | |||
| sess := x.NewSession() | |||
| defer sess.Close() | |||
| var cond = builder.NewCond() | |||
| if opts.RepoID > 0 { | |||
| cond.And( | |||
| builder.Eq{"cloudbrain.repo_id": opts.RepoID}, | |||
| ) | |||
| } | |||
| if opts.UserID > 0 { | |||
| cond.And( | |||
| builder.Eq{"cloudbrain.user_id": opts.UserID}, | |||
| ) | |||
| } | |||
| if (opts.JobID) > 0 { | |||
| cond.And( | |||
| builder.Eq{"cloudbrain.job_id": opts.JobID}, | |||
| ) | |||
| } | |||
| // switch opts.JobStatus { | |||
| // case JobWaiting: | |||
| // cond.And(builder.Eq{"cloudbrain.status": int(JobWaiting)}) | |||
| // case JobFailed: | |||
| // cond.And(builder.Eq{"cloudbrain.status": int(JobFailed)}) | |||
| // case JobStopped: | |||
| // cond.And(builder.Eq{"cloudbrain.status": int(JobStopped)}) | |||
| // case JobSucceeded: | |||
| // cond.And(builder.Eq{"cloudbrain.status": int(JobSucceeded)}) | |||
| // } | |||
| if len(opts.CloudbrainIDs) > 0 { | |||
| cond.And(builder.In("cloudbrain.id", opts.CloudbrainIDs)) | |||
| } | |||
| count, err := sess.Where(cond).Count(new(Cloudbrain)) | |||
| if err != nil { | |||
| return nil, 0, fmt.Errorf("Count: %v", err) | |||
| } | |||
| if opts.Page >= 0 && opts.PageSize > 0 { | |||
| var start int | |||
| if opts.Page == 0 { | |||
| start = 0 | |||
| } else { | |||
| start = (opts.Page - 1) * opts.PageSize | |||
| } | |||
| sess.Limit(opts.PageSize, start) | |||
| } | |||
| sess.OrderBy("cloudbrain.created_unix DESC") | |||
| cloudbrains := make([]*Cloudbrain, 0, setting.UI.IssuePagingNum) | |||
| if err := sess.Find(&cloudbrains); err != nil { | |||
| return nil, 0, fmt.Errorf("Find: %v", err) | |||
| } | |||
| sess.Close() | |||
| return cloudbrains, count, nil | |||
| } | |||
| func CreateCloudbrain(cloudbrain *Cloudbrain) (err error) { | |||
| if _, err = x.Insert(cloudbrain); err != nil { | |||
| return err | |||
| } | |||
| return nil | |||
| } | |||
| func getRepoCloudBrain(cb *Cloudbrain) (*Cloudbrain, error) { | |||
| has, err := x.Get(cb) | |||
| if err != nil { | |||
| return nil, err | |||
| } else if !has { | |||
| return nil, errors.New("cloudbrain task is not found") | |||
| } | |||
| return cb, nil | |||
| } | |||
| func GetRepoCloudBrainByJobID(repoID int64, jobID string) (*Cloudbrain, error) { | |||
| cb := &Cloudbrain{JobID: jobID, RepoID: repoID} | |||
| return getRepoCloudBrain(cb) | |||
| } | |||
| func GetCloudbrainByJobID(jobID string) (*Cloudbrain, error) { | |||
| cb := &Cloudbrain{JobID: jobID} | |||
| return getRepoCloudBrain(cb) | |||
| } | |||
| func SetCloudbrainStatusByJobID(jobID string, status CloudbrainStatus) (err error) { | |||
| cb := &Cloudbrain{JobID: jobID, Status: string(status)} | |||
| _, err = x.Cols("status").Where("cloudbrain.job_id=?", jobID).Update(cb) | |||
| return | |||
| } | |||
| @@ -213,6 +213,7 @@ var migrations = []Migration{ | |||
| // v139 -> v140 | |||
| NewMigration("prepend refs/heads/ to issue refs", prependRefsHeadsToIssueRefs), | |||
| NewMigration("add dataset migration", addDatasetTable), | |||
| NewMigration("add cloudbrain migration", addCloudBrainTable), | |||
| } | |||
| // GetCurrentDBVersion returns the current db version | |||
| @@ -0,0 +1,26 @@ | |||
| package migrations | |||
| import ( | |||
| "fmt" | |||
| "code.gitea.io/gitea/modules/timeutil" | |||
| "xorm.io/xorm" | |||
| ) | |||
| func addCloudBrainTable(x *xorm.Engine) error { | |||
| type Cloudbrain struct { | |||
| ID int64 `xorm:"pk autoincr"` | |||
| JobID string `xorm:"INDEX NOT NULL"` | |||
| JobName string | |||
| Status string `xorm:"INDEX"` | |||
| UserID int64 `xorm:"INDEX"` | |||
| RepoID int64 `xorm:"INDEX"` | |||
| CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` | |||
| UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` | |||
| } | |||
| if err := x.Sync2(new(Cloudbrain)); err != nil { | |||
| return fmt.Errorf("Sync2: %v", err) | |||
| } | |||
| return nil | |||
| } | |||
| @@ -126,6 +126,7 @@ func init() { | |||
| new(LanguageStat), | |||
| new(EmailHash), | |||
| new(Dataset), | |||
| new(Cloudbrain), | |||
| ) | |||
| gonicNames := []string{"SSL", "UID"} | |||
| @@ -1076,6 +1076,12 @@ func CreateRepository(ctx DBContext, doer, u *User, repo *Repository) (err error | |||
| Type: tp, | |||
| Config: &DatasetConfig{EnableDataset: true}, | |||
| }) | |||
| } else if tp == UnitTypeCloudBrain { | |||
| units = append(units, RepoUnit{ | |||
| RepoID: repo.ID, | |||
| Type: tp, | |||
| Config: &CloudBrainConfig{EnableCloudBrain: true}, | |||
| }) | |||
| } else { | |||
| units = append(units, RepoUnit{ | |||
| RepoID: repo.ID, | |||
| @@ -117,16 +117,30 @@ type DatasetConfig struct { | |||
| EnableDataset bool | |||
| } | |||
| // FromDB fills up a IssuesConfig from serialized format. | |||
| // FromDB fills up a DatasetConfig from serialized format. | |||
| func (cfg *DatasetConfig) FromDB(bs []byte) error { | |||
| return json.Unmarshal(bs, &cfg) | |||
| } | |||
| // ToDB exports a IssuesConfig to a serialized format. | |||
| // ToDB exports a DatasetConfig to a serialized format. | |||
| func (cfg *DatasetConfig) ToDB() ([]byte, error) { | |||
| return json.Marshal(cfg) | |||
| } | |||
| type CloudBrainConfig struct { | |||
| EnableCloudBrain bool | |||
| } | |||
| // FromDB fills up a CloudBrainConfig from serialized format. | |||
| func (cfg *CloudBrainConfig) FromDB(bs []byte) error { | |||
| return json.Unmarshal(bs, &cfg) | |||
| } | |||
| // ToDB exports a CloudBrainConfig to a serialized format. | |||
| func (cfg *CloudBrainConfig) ToDB() ([]byte, error) { | |||
| return json.Marshal(cfg) | |||
| } | |||
| // BeforeSet is invoked from XORM before setting the value of a field of this object. | |||
| func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { | |||
| switch colName { | |||
| @@ -144,6 +158,8 @@ func (r *RepoUnit) BeforeSet(colName string, val xorm.Cell) { | |||
| r.Config = new(IssuesConfig) | |||
| case UnitTypeDatasets: | |||
| r.Config = new(DatasetConfig) | |||
| case UnitTypeCloudBrain: | |||
| r.Config = new(CloudBrainConfig) | |||
| default: | |||
| panic("unrecognized repo unit type: " + com.ToStr(*val)) | |||
| } | |||
| @@ -25,6 +25,7 @@ const ( | |||
| UnitTypeExternalWiki // 6 ExternalWiki | |||
| UnitTypeExternalTracker // 7 ExternalTracker | |||
| UnitTypeDatasets UnitType = 10 // 10 Dataset | |||
| UnitTypeCloudBrain UnitType = 11 // 11 CloudBrain | |||
| ) | |||
| // Value returns integer value for unit type | |||
| @@ -50,6 +51,8 @@ func (u UnitType) String() string { | |||
| return "UnitTypeExternalTracker" | |||
| case UnitTypeDatasets: | |||
| return "UnitTypeDataset" | |||
| case UnitTypeCloudBrain: | |||
| return "UnitTypeCloudBrain" | |||
| } | |||
| return fmt.Sprintf("Unknown UnitType %d", u) | |||
| } | |||
| @@ -72,6 +75,7 @@ var ( | |||
| UnitTypeExternalWiki, | |||
| UnitTypeExternalTracker, | |||
| UnitTypeDatasets, | |||
| UnitTypeCloudBrain, | |||
| } | |||
| // DefaultRepoUnits contains the default unit types | |||
| @@ -82,6 +86,7 @@ var ( | |||
| UnitTypeReleases, | |||
| UnitTypeWiki, | |||
| UnitTypeDatasets, | |||
| UnitTypeCloudBrain, | |||
| } | |||
| // NotAllowedDefaultRepoUnits contains units that can't be default | |||
| @@ -255,6 +260,14 @@ var ( | |||
| 5, | |||
| } | |||
| UnitCloudBrain = Unit{ | |||
| UnitTypeCloudBrain, | |||
| "repo.cloudbrains", | |||
| "/cloudbrains", | |||
| "repo.cloudbrains.desc", | |||
| 6, | |||
| } | |||
| // Units contains all the units | |||
| Units = map[UnitType]Unit{ | |||
| UnitTypeCode: UnitCode, | |||
| @@ -265,6 +278,7 @@ var ( | |||
| UnitTypeWiki: UnitWiki, | |||
| UnitTypeExternalWiki: UnitExternalWiki, | |||
| UnitTypeDatasets: UnitDataset, | |||
| UnitTypeCloudBrain: UnitCloudBrain, | |||
| } | |||
| ) | |||
| @@ -0,0 +1,17 @@ | |||
| package auth | |||
| import ( | |||
| "gitea.com/macaron/binding" | |||
| "gitea.com/macaron/macaron" | |||
| ) | |||
| // CreateDatasetForm form for dataset page | |||
| type CreateCloudBrainForm struct { | |||
| JobName string `form:"job_name" binding:"Required"` | |||
| Image string `binding:"Required"` | |||
| Command string `binding:"Required"` | |||
| } | |||
| func (f *CreateCloudBrainForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { | |||
| return validate(errs, ctx.Data, f, ctx.Locale) | |||
| } | |||
| @@ -121,6 +121,7 @@ type RepoSettingForm struct { | |||
| // Advanced settings | |||
| EnableDataset bool | |||
| EnableCloudBrain bool | |||
| EnableWiki bool | |||
| EnableExternalWiki bool | |||
| ExternalWikiURL string | |||
| @@ -0,0 +1,54 @@ | |||
| package cloudbrain | |||
| import ( | |||
| "errors" | |||
| "code.gitea.io/gitea/modules/context" | |||
| "code.gitea.io/gitea/models" | |||
| ) | |||
| func GenerateTask(ctx *context.Context, jobName, image, command string) error { | |||
| jobResult, err := CreateJob(jobName, models.CreateJobParams{ | |||
| JobName: jobName, | |||
| RetryCount: 1, | |||
| GpuType: "dgx", | |||
| Image: image, | |||
| TaskRoles: []models.TaskRole{ | |||
| { | |||
| Name: "task1", | |||
| TaskNumber: 1, | |||
| MinSucceededTaskCount: 1, | |||
| MinFailedTaskCount: 1, | |||
| CPUNumber: 2, | |||
| GPUNumber: 1, | |||
| MemoryMB: 16384, | |||
| ShmMB: 8192, | |||
| Command: command, | |||
| NeedIBDevice: false, | |||
| IsMainRole: false, | |||
| }, | |||
| }, | |||
| }) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| if jobResult.Code != "S000" { | |||
| return errors.New(jobResult.Msg) | |||
| } | |||
| var jobID = jobResult.Payload["jobId"].(string) | |||
| err = models.CreateCloudbrain(&models.Cloudbrain{ | |||
| Status: string(models.JobWaiting), | |||
| UserID: ctx.User.ID, | |||
| RepoID: ctx.Repo.Repository.ID, | |||
| JobID: jobID, | |||
| JobName: jobName, | |||
| }) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| return nil | |||
| } | |||
| @@ -0,0 +1,120 @@ | |||
| package cloudbrain | |||
| import ( | |||
| "fmt" | |||
| "code.gitea.io/gitea/models" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| resty "github.com/go-resty/resty/v2" | |||
| ) | |||
| var ( | |||
| restyClient *resty.Client | |||
| HOST string | |||
| TOKEN string | |||
| ) | |||
| func getRestyClient() *resty.Client { | |||
| if restyClient == nil { | |||
| restyClient = resty.New() | |||
| } | |||
| return restyClient | |||
| } | |||
| func checkSetting() { | |||
| if len(HOST) != 0 && len(TOKEN) != 0 && restyClient != nil { | |||
| return | |||
| } | |||
| _ = loginCloudbrain() | |||
| } | |||
| func loginCloudbrain() error { | |||
| conf := setting.GetCloudbrainConfig() | |||
| username := conf.Username | |||
| password := conf.Password | |||
| HOST = conf.Host | |||
| var loginResult models.CloudBrainLoginResult | |||
| client := getRestyClient() | |||
| res, err := client.R(). | |||
| SetHeader("Content-Type", "application/json"). | |||
| SetBody(map[string]interface{}{"username": username, "password": password, "expiration": "604800"}). | |||
| SetResult(&loginResult). | |||
| Post(HOST + "/rest-server/api/v1/token") | |||
| if err != nil { | |||
| return fmt.Errorf("resty loginCloudbrain: %s", err) | |||
| } | |||
| if loginResult.Code != "S000" { | |||
| return fmt.Errorf("%s: %s", loginResult.Msg, res.String()) | |||
| } | |||
| TOKEN = loginResult.Payload["token"].(string) | |||
| return nil | |||
| } | |||
| func CreateJob(jobName string, createJobParams models.CreateJobParams) (*models.CreateJobResult, error) { | |||
| checkSetting() | |||
| client := getRestyClient() | |||
| var jobResult models.CreateJobResult | |||
| retry := 0 | |||
| sendjob: | |||
| res, err := client.R(). | |||
| SetHeader("Content-Type", "application/json"). | |||
| SetAuthToken(TOKEN). | |||
| SetBody(createJobParams). | |||
| SetResult(&jobResult). | |||
| Put(HOST + "/rest-server/api/v1/jobs/" + jobName) | |||
| if err != nil { | |||
| return nil, fmt.Errorf("resty create job: %s", err) | |||
| } | |||
| if jobResult.Code == "S401" && retry < 1 { | |||
| retry++ | |||
| _ = loginCloudbrain() | |||
| goto sendjob | |||
| } | |||
| if jobResult.Code != "S000" { | |||
| return &jobResult, fmt.Errorf("jobResult err: %s", res.String()) | |||
| } | |||
| return &jobResult, nil | |||
| } | |||
| func GetJob(jobID string) (*models.GetJobResult, error) { | |||
| checkSetting() | |||
| // http://192.168.204.24/rest-server/api/v1/jobs/90e26e500c4b3011ea0a251099a987938b96 | |||
| client := getRestyClient() | |||
| var getJobResult models.GetJobResult | |||
| retry := 0 | |||
| sendjob: | |||
| res, err := client.R(). | |||
| SetHeader("Content-Type", "application/json"). | |||
| SetAuthToken(TOKEN). | |||
| SetResult(&getJobResult). | |||
| Get(HOST + "/rest-server/api/v1/jobs/" + jobID) | |||
| if err != nil { | |||
| return nil, fmt.Errorf("resty GetJob: %s", err) | |||
| } | |||
| if getJobResult.Code == "S401" && retry < 1 { | |||
| retry++ | |||
| _ = loginCloudbrain() | |||
| goto sendjob | |||
| } | |||
| if getJobResult.Code != "S000" { | |||
| return &getJobResult, fmt.Errorf("jobResult GetJob err: %s", res.String()) | |||
| } | |||
| return &getJobResult, nil | |||
| } | |||
| @@ -814,6 +814,7 @@ func UnitTypes() macaron.Handler { | |||
| ctx.Data["UnitTypeCode"] = models.UnitTypeCode | |||
| ctx.Data["UnitTypeIssues"] = models.UnitTypeIssues | |||
| ctx.Data["UnitTypeDatasets"] = models.UnitTypeDatasets | |||
| ctx.Data["UnitTypeCloudBrain"] = models.UnitTypeCloudBrain | |||
| ctx.Data["UnitTypePullRequests"] = models.UnitTypePullRequests | |||
| ctx.Data["UnitTypeReleases"] = models.UnitTypeReleases | |||
| ctx.Data["UnitTypeWiki"] = models.UnitTypeWiki | |||
| @@ -0,0 +1,19 @@ | |||
| package setting | |||
| type CloudbrainLoginConfig struct { | |||
| Username string | |||
| Password string | |||
| Host string | |||
| } | |||
| var ( | |||
| Cloudbrain = CloudbrainLoginConfig{} | |||
| ) | |||
| func GetCloudbrainConfig() CloudbrainLoginConfig { | |||
| cloudbrainSec := Cfg.Section("cloudbrain") | |||
| Cloudbrain.Username = cloudbrainSec.Key("USERNAME").MustString("") | |||
| Cloudbrain.Password = cloudbrainSec.Key("PASSWORD").MustString("") | |||
| Cloudbrain.Host = cloudbrainSec.Key("HOST").MustString("") | |||
| return Cloudbrain | |||
| } | |||
| @@ -134,6 +134,9 @@ func NewFuncMap() []template.FuncMap { | |||
| "EscapePound": func(str string) string { | |||
| return strings.NewReplacer("%", "%25", "#", "%23", " ", "%20", "?", "%3F").Replace(str) | |||
| }, | |||
| "nl2br": func(text string) template.HTML { | |||
| return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "<br>", -1)) | |||
| }, | |||
| "PathEscapeSegments": util.PathEscapeSegments, | |||
| "URLJoin": util.URLJoin, | |||
| "RenderCommitMessage": RenderCommitMessage, | |||
| @@ -849,6 +849,9 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| Delete(reqToken(), repo.DeleteTopic) | |||
| }, reqAdmin()) | |||
| }, reqAnyRepoReader()) | |||
| m.Group("/cloudbrain", func() { | |||
| m.Get("/:jobid", repo.GetCloudbrainTask) | |||
| }, reqRepoReader(models.UnitTypeCloudBrain)) | |||
| }, repoAssignment()) | |||
| }) | |||
| @@ -0,0 +1,79 @@ | |||
| // Copyright 2016 The Gogs Authors. All rights reserved. | |||
| // Copyright 2018 The Gitea Authors. All rights reserved. | |||
| // Use of this source code is governed by a MIT-style | |||
| // license that can be found in the LICENSE file. | |||
| package repo | |||
| import ( | |||
| "net/http" | |||
| "time" | |||
| "code.gitea.io/gitea/models" | |||
| "code.gitea.io/gitea/modules/cloudbrain" | |||
| "code.gitea.io/gitea/modules/context" | |||
| ) | |||
| // cloudbrain get job task by jobid | |||
| func GetCloudbrainTask(ctx *context.APIContext) { | |||
| // swagger:operation GET /repos/{owner}/{repo}/cloudbrain/{jobid} cloudbrain jobTask | |||
| // --- | |||
| // summary: Get a single task | |||
| // produces: | |||
| // - application/json | |||
| // parameters: | |||
| // - name: owner | |||
| // in: path | |||
| // description: owner of the repo | |||
| // type: string | |||
| // required: true | |||
| // - name: repo | |||
| // in: path | |||
| // description: name of the repo | |||
| // type: string | |||
| // required: true | |||
| // - name: jobid | |||
| // in: path | |||
| // description: id of cloudbrain jobid | |||
| // type: string | |||
| // required: true | |||
| // responses: | |||
| // "200": | |||
| // "$ref": "#/responses/Label" | |||
| var ( | |||
| err error | |||
| ) | |||
| jobID := ctx.Params(":jobid") | |||
| repoID := ctx.Repo.Repository.ID | |||
| _, err = models.GetRepoCloudBrainByJobID(repoID, jobID) | |||
| if err != nil { | |||
| ctx.NotFound(err) | |||
| return | |||
| } | |||
| jobResult, err := cloudbrain.GetJob(jobID) | |||
| if err != nil { | |||
| ctx.NotFound(err) | |||
| return | |||
| } | |||
| result, err := models.ConvertToJobResultPayload(jobResult.Payload) | |||
| if err != nil { | |||
| ctx.NotFound(err) | |||
| return | |||
| } | |||
| if result.JobStatus.State != string(models.JobWaiting) { | |||
| go models.SetCloudbrainStatusByJobID(result.Config.JobID, models.CloudbrainStatus(result.JobStatus.State)) | |||
| } | |||
| ctx.JSON(http.StatusOK, map[string]interface{}{ | |||
| "JobID": result.Config.JobID, | |||
| "JobStatus": result.JobStatus.State, | |||
| "SubState": result.JobStatus.SubState, | |||
| "CreatedTime": time.Unix(result.JobStatus.CreatedTime/1000, 0).Format("2006-01-02 15:04:05"), | |||
| "CompletedTime": time.Unix(result.JobStatus.CompletedTime/1000, 0).Format("2006-01-02 15:04:05"), | |||
| }) | |||
| } | |||
| @@ -0,0 +1,114 @@ | |||
| package repo | |||
| import ( | |||
| "strconv" | |||
| "time" | |||
| "code.gitea.io/gitea/models" | |||
| "code.gitea.io/gitea/modules/auth" | |||
| "code.gitea.io/gitea/modules/base" | |||
| "code.gitea.io/gitea/modules/cloudbrain" | |||
| "code.gitea.io/gitea/modules/context" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| ) | |||
| const ( | |||
| tplCloudBrainIndex base.TplName = "repo/cloudbrain/index" | |||
| tplCloudBrainNew base.TplName = "repo/cloudbrain/new" | |||
| tplCloudBrainShow base.TplName = "repo/cloudbrain/show" | |||
| ) | |||
| // MustEnableDataset check if repository enable internal cb | |||
| func MustEnableCloudbrain(ctx *context.Context) { | |||
| if !ctx.Repo.CanRead(models.UnitTypeCloudBrain) { | |||
| ctx.NotFound("MustEnableCloudbrain", nil) | |||
| return | |||
| } | |||
| } | |||
| func CloudBrainIndex(ctx *context.Context) { | |||
| MustEnableCloudbrain(ctx) | |||
| repo := ctx.Repo.Repository | |||
| page := ctx.QueryInt("page") | |||
| if page <= 0 { | |||
| page = 1 | |||
| } | |||
| ciTasks, count, err := models.Cloudbrains(&models.CloudbrainsOptions{ | |||
| ListOptions: models.ListOptions{ | |||
| Page: page, | |||
| PageSize: setting.UI.IssuePagingNum, | |||
| }, | |||
| RepoID: repo.ID, | |||
| // SortType: sortType, | |||
| }) | |||
| if err != nil { | |||
| ctx.ServerError("Cloudbrain", err) | |||
| return | |||
| } | |||
| pager := context.NewPagination(int(count), setting.UI.IssuePagingNum, page, 5) | |||
| pager.SetDefaultParams(ctx) | |||
| ctx.Data["Page"] = pager | |||
| ctx.Data["PageIsCloudBrain"] = true | |||
| ctx.Data["Tasks"] = ciTasks | |||
| ctx.HTML(200, tplCloudBrainIndex) | |||
| } | |||
| func cutString(str string, lens int) string { | |||
| if len(str) < lens { | |||
| return str | |||
| } | |||
| return str[:lens] | |||
| } | |||
| func CloudBrainNew(ctx *context.Context) { | |||
| ctx.Data["PageIsCloudBrain"] = true | |||
| t := time.Now() | |||
| var jobName = cutString(ctx.User.Name, 5) + t.Format("2006010215") + strconv.Itoa(int(t.Unix()))[:5] | |||
| ctx.Data["job_name"] = jobName | |||
| ctx.Data["image"] = "192.168.202.74:5000/user-images/deepo:v2.0" | |||
| ctx.Data["command"] = `pip3 install jupyterlab==1.1.4;service ssh stop;jupyter lab --no-browser --ip=0.0.0.0 --allow-root --notebook-dir=\"/userhome\" --port=80 --NotebookApp.token=\"\" --LabApp.allow_origin=\"self https://cloudbrain.pcl.ac.cn\"` | |||
| ctx.HTML(200, tplCloudBrainNew) | |||
| } | |||
| func CloudBrainCreate(ctx *context.Context, form auth.CreateCloudBrainForm) { | |||
| ctx.Data["PageIsCloudBrain"] = true | |||
| jobName := form.JobName | |||
| image := form.Image | |||
| command := form.Command | |||
| err := cloudbrain.GenerateTask(ctx, jobName, image, command) | |||
| if err != nil { | |||
| ctx.RenderWithErr(err.Error(), tplCloudBrainNew, &form) | |||
| return | |||
| } | |||
| ctx.Redirect(setting.AppSubURL + ctx.Repo.RepoLink + "/cloudbrain") | |||
| } | |||
| func CloudBrainShow(ctx *context.Context) { | |||
| ctx.Data["PageIsCloudBrain"] = true | |||
| var jobID = ctx.Params(":jobid") | |||
| task, err := models.GetCloudbrainByJobID(jobID) | |||
| if err != nil { | |||
| ctx.Data["error"] = err.Error() | |||
| } | |||
| ctx.Data["task"] = task | |||
| result, err := cloudbrain.GetJob(jobID) | |||
| if err != nil { | |||
| ctx.Data["error"] = err.Error() | |||
| } | |||
| if result != nil { | |||
| jobRes, _ := models.ConvertToJobResultPayload(result.Payload) | |||
| ctx.Data["result"] = jobRes | |||
| taskRoles := jobRes.TaskRoles | |||
| taskRes, _ := models.ConvertToTaskPod(taskRoles["task1"].(map[string]interface{})) | |||
| ctx.Data["taskRes"] = taskRes | |||
| } | |||
| ctx.Data["jobID"] = jobID | |||
| ctx.HTML(200, tplCloudBrainShow) | |||
| } | |||
| @@ -227,6 +227,18 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { | |||
| deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeDatasets) | |||
| } | |||
| if form.EnableCloudBrain && !models.UnitTypeCloudBrain.UnitGlobalDisabled() { | |||
| units = append(units, models.RepoUnit{ | |||
| RepoID: repo.ID, | |||
| Type: models.UnitTypeCloudBrain, | |||
| Config: &models.CloudBrainConfig{ | |||
| EnableCloudBrain: form.EnableCloudBrain, | |||
| }, | |||
| }) | |||
| } else if !models.UnitTypeCloudBrain.UnitGlobalDisabled() { | |||
| deleteUnitTypes = append(deleteUnitTypes, models.UnitTypeCloudBrain) | |||
| } | |||
| if form.EnableWiki && form.EnableExternalWiki && !models.UnitTypeExternalWiki.UnitGlobalDisabled() { | |||
| if !validation.IsValidExternalURL(form.ExternalWikiURL) { | |||
| ctx.Flash.Error(ctx.Tr("repo.settings.external_wiki_url_error")) | |||
| @@ -548,6 +548,8 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| reqRepoIssuesOrPullsReader := context.RequireRepoReaderOr(models.UnitTypeIssues, models.UnitTypePullRequests) | |||
| reqRepoDatasetReader := context.RequireRepoReader(models.UnitTypeDatasets) | |||
| reqRepoDatasetWriter := context.RequireRepoWriter(models.UnitTypeDatasets) | |||
| reqRepoCloudBrainReader := context.RequireRepoReader(models.UnitTypeCloudBrain) | |||
| reqRepoCloudBrainWriter := context.RequireRepoWriter(models.UnitTypeCloudBrain) | |||
| // ***** START: Organization ***** | |||
| m.Group("/org", func() { | |||
| @@ -873,6 +875,13 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| m.Post("", reqRepoDatasetWriter, bindIgnErr(auth.EditDatasetForm{}), repo.EditDatasetPost) | |||
| }, context.RepoRef()) | |||
| m.Group("/cloudbrain", func() { | |||
| m.Get("", reqRepoCloudBrainReader, repo.CloudBrainIndex) | |||
| m.Get("/:jobid", reqRepoCloudBrainReader, repo.CloudBrainShow) | |||
| m.Get("/create", reqRepoCloudBrainWriter, repo.CloudBrainNew) | |||
| m.Post("/create", reqRepoCloudBrainWriter, bindIgnErr(auth.CreateCloudBrainForm{}), repo.CloudBrainCreate) | |||
| }, context.RepoRef()) | |||
| m.Group("/wiki", func() { | |||
| m.Get("/?:page", repo.Wiki) | |||
| m.Get("/_pages", repo.WikiPages) | |||
| @@ -50,7 +50,7 @@ | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <a class="flip" href="/html/2020/Framework_0325/18.html"></a> | |||
| <a class="flip" href="https://www.openi.org.cn/html/2020/Framework_0325/18.html"></a> | |||
| </div> | |||
| <div class="eight wide mobile four wide tablet four wide computer column ipros"> | |||
| <div class="ui card"> | |||
| @@ -90,7 +90,7 @@ | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <a class="flip" href="/html/2020/Framework_0325/18.html"></a> | |||
| <a class="flip" href="https://www.openi.org.cn/html/2020/Framework_0325/18.html"></a> | |||
| </div> | |||
| <div class="sixteen wide mobile two wide tablet two wide computer column"> | |||
| <div class="ui card"> | |||
| @@ -118,7 +118,7 @@ | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <a class="flip flip-blue" href="/html/2020/Environment_0325/9.html"></a> | |||
| <a class="flip flip-blue" href="https://www.openi.org.cn/html/2020/Environment_0325/9.html"></a> | |||
| </div> | |||
| <div class="eight wide mobile three wide tablet three wide computer column ipros"> | |||
| <div class="ui card"> | |||
| @@ -134,7 +134,7 @@ | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <a class="flip flip-blue" href="/html/2020/Environment_0325/10.html"></a> | |||
| <a class="flip flip-blue" href="https://www.openi.org.cn/html/2020/Environment_0325/10.html"></a> | |||
| </div> | |||
| <div class="five wide mobile three wide tablet three wide computer column ipros"> | |||
| <div class="ui card"> | |||
| @@ -150,7 +150,7 @@ | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <a class="flip flip-green" href="/html/2020/Environment_0325/11.html"></a> | |||
| <a class="flip flip-green" href="https://www.openi.org.cn/html/2020/Environment_0325/11.html"></a> | |||
| </div> | |||
| <div class="five wide mobile three wide tablet three wide computer column ipros"> | |||
| <div class="ui card"> | |||
| @@ -177,7 +177,7 @@ | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <a class="flip flip-green" href="/html/2020/Environment_0325/13.html"></a> | |||
| <a class="flip flip-green" href="https://www.openi.org.cn/html/2020/Environment_0325/13.html"></a> | |||
| </div> | |||
| </div> | |||
| <div class="ui divider" style="margin-top:2.0em; margin-bottom:2.0em;"></div> | |||
| @@ -215,7 +215,7 @@ | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <a class="flip flip-green" href="/html/2020/Framework_0325/15.html"></a> | |||
| <a class="flip flip-green" href="https://www.openi.org.cn/html/2020/Framework_0325/15.html"></a> | |||
| </div> | |||
| <div class="eight wide mobile three wide tablet four wide computer column ipros"> | |||
| <div class="ui card"> | |||
| @@ -1,103 +1,103 @@ | |||
| {{template "base/head" .}} | |||
| <div class="organization settings options"> | |||
| {{template "org/header" .}} | |||
| <div class="ui container"> | |||
| <div class="ui grid"> | |||
| {{template "org/settings/navbar" .}} | |||
| <div class="twelve wide column content"> | |||
| {{template "base/alert" .}} | |||
| <h4 class="ui top attached header"> | |||
| {{.i18n.Tr "org.settings.options"}} | |||
| </h4> | |||
| <div class="ui attached segment"> | |||
| <form class="ui form" action="{{.Link}}" method="post"> | |||
| {{.CsrfTokenHtml}} | |||
| <div class="required field {{if .Err_Name}}error{{end}}"> | |||
| <label for="org_name">{{.i18n.Tr "org.org_name_holder"}}<span class="text red hide" id="org-name-change-prompt"> {{.i18n.Tr "org.settings.change_orgname_prompt"}}</span></label> | |||
| <input id="org_name" name="name" value="{{.Org.Name}}" data-org-name="{{.Org.Name}}" autofocus required> | |||
| </div> | |||
| <div class="field {{if .Err_FullName}}error{{end}}"> | |||
| <label for="full_name">{{.i18n.Tr "org.org_full_name_holder"}}</label> | |||
| <input id="full_name" name="full_name" value="{{.Org.FullName}}"> | |||
| </div> | |||
| <div class="field {{if .Err_Description}}error{{end}}"> | |||
| <label for="description">{{$.i18n.Tr "org.org_desc"}}</label> | |||
| <textarea id="description" name="description" rows="2">{{.Org.Description}}</textarea> | |||
| </div> | |||
| <div class="field {{if .Err_Website}}error{{end}}"> | |||
| <label for="website">{{.i18n.Tr "org.settings.website"}}</label> | |||
| <input id="website" name="website" type="url" value="{{.Org.Website}}"> | |||
| </div> | |||
| <div class="field"> | |||
| <label for="location">{{.i18n.Tr "org.settings.location"}}</label> | |||
| <input id="location" name="location" value="{{.Org.Location}}"> | |||
| </div> | |||
| {{template "org/header" .}} | |||
| <div class="ui container"> | |||
| <div class="ui grid"> | |||
| {{template "org/settings/navbar" .}} | |||
| <div class="twelve wide column content"> | |||
| {{template "base/alert" .}} | |||
| <h4 class="ui top attached header"> | |||
| {{.i18n.Tr "org.settings.options"}} | |||
| </h4> | |||
| <div class="ui attached segment"> | |||
| <form class="ui form" action="{{.Link}}" method="post"> | |||
| {{.CsrfTokenHtml}} | |||
| <div class="required field {{if .Err_Name}}error{{end}}"> | |||
| <label for="org_name">{{.i18n.Tr "org.org_name_holder"}}<span class="text red hide" id="org-name-change-prompt"> {{.i18n.Tr "org.settings.change_orgname_prompt"}}</span></label> | |||
| <input id="org_name" name="name" value="{{.Org.Name}}" data-org-name="{{.Org.Name}}" autofocus required> | |||
| </div> | |||
| <div class="field {{if .Err_FullName}}error{{end}}"> | |||
| <label for="full_name">{{.i18n.Tr "org.org_full_name_holder"}}</label> | |||
| <input id="full_name" name="full_name" value="{{.Org.FullName}}"> | |||
| </div> | |||
| <div class="field {{if .Err_Description}}error{{end}}"> | |||
| <label for="description">{{$.i18n.Tr "org.org_desc"}}</label> | |||
| <textarea id="description" name="description" rows="2">{{.Org.Description}}</textarea> | |||
| </div> | |||
| <div class="field {{if .Err_Website}}error{{end}}"> | |||
| <label for="website">{{.i18n.Tr "org.settings.website"}}</label> | |||
| <input id="website" name="website" type="url" value="{{.Org.Website}}"> | |||
| </div> | |||
| <div class="field"> | |||
| <label for="location">{{.i18n.Tr "org.settings.location"}}</label> | |||
| <input id="location" name="location" value="{{.Org.Location}}"> | |||
| </div> | |||
| <div class="ui divider"></div> | |||
| <div class="field" id="visibility_box"> | |||
| <label for="visibility">{{.i18n.Tr "org.settings.visibility"}}</label> | |||
| <div class="field"> | |||
| <div class="ui radio checkbox"> | |||
| <input class="hidden enable-system-radio" tabindex="0" name="visibility" type="radio" value="0" {{if eq .CurrentVisibility 0}}checked{{end}}/> | |||
| <label>{{.i18n.Tr "org.settings.visibility.public"}}</label> | |||
| </div> | |||
| </div> | |||
| <div class="field"> | |||
| <div class="ui radio checkbox"> | |||
| <input class="hidden enable-system-radio" tabindex="0" name="visibility" type="radio" value="1" {{if eq .CurrentVisibility 1}}checked{{end}}/> | |||
| <label>{{.i18n.Tr "org.settings.visibility.limited"}}</label> | |||
| </div> | |||
| </div> | |||
| <div class="field"> | |||
| <div class="ui radio checkbox"> | |||
| <input class="hidden enable-system-radio" tabindex="0" name="visibility" type="radio" value="2" {{if eq .CurrentVisibility 2}}checked{{end}}/> | |||
| <label>{{.i18n.Tr "org.settings.visibility.private"}}</label> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div class="ui divider"></div> | |||
| <div class="field" id="visibility_box"> | |||
| <label for="visibility">{{.i18n.Tr "org.settings.visibility"}}</label> | |||
| <div class="field"> | |||
| <div class="ui radio checkbox"> | |||
| <input class="hidden enable-system-radio" tabindex="0" name="visibility" type="radio" value="0" {{if eq .CurrentVisibility 0}}checked{{end}}/> | |||
| <label>{{.i18n.Tr "org.settings.visibility.public"}}</label> | |||
| </div> | |||
| </div> | |||
| <div class="field"> | |||
| <div class="ui radio checkbox"> | |||
| <input class="hidden enable-system-radio" tabindex="0" name="visibility" type="radio" value="1" {{if eq .CurrentVisibility 1}}checked{{end}}/> | |||
| <label>{{.i18n.Tr "org.settings.visibility.limited"}}</label> | |||
| </div> | |||
| </div> | |||
| <div class="field"> | |||
| <div class="ui radio checkbox"> | |||
| <input class="hidden enable-system-radio" tabindex="0" name="visibility" type="radio" value="2" {{if eq .CurrentVisibility 2}}checked{{end}}/> | |||
| <label>{{.i18n.Tr "org.settings.visibility.private"}}</label> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div class="field" id="permission_box"> | |||
| <label>{{.i18n.Tr "org.settings.permission"}}</label> | |||
| <div class="field"> | |||
| <div class="ui checkbox"> | |||
| <input class="hidden" type="checkbox" name="repo_admin_change_team_access" {{if .RepoAdminChangeTeamAccess}}checked{{end}}/> | |||
| <label>{{.i18n.Tr "org.settings.repoadminchangeteam"}}</label> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div class="field" id="permission_box"> | |||
| <label>{{.i18n.Tr "org.settings.permission"}}</label> | |||
| <div class="field"> | |||
| <div class="ui checkbox"> | |||
| <input class="hidden" type="checkbox" name="repo_admin_change_team_access" {{if .RepoAdminChangeTeamAccess}}checked{{end}}/> | |||
| <label>{{.i18n.Tr "org.settings.repoadminchangeteam"}}</label> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| {{if .SignedUser.IsAdmin}} | |||
| <div class="ui divider"></div> | |||
| {{if .SignedUser.IsAdmin}} | |||
| <div class="ui divider"></div> | |||
| <div class="inline field {{if .Err_MaxRepoCreation}}error{{end}}"> | |||
| <label for="max_repo_creation">{{.i18n.Tr "admin.users.max_repo_creation"}}</label> | |||
| <input id="max_repo_creation" name="max_repo_creation" type="number" value="{{.Org.MaxRepoCreation}}"> | |||
| <p class="help">{{.i18n.Tr "admin.users.max_repo_creation_desc"}}</p> | |||
| </div> | |||
| {{end}} | |||
| <div class="inline field {{if .Err_MaxRepoCreation}}error{{end}}"> | |||
| <label for="max_repo_creation">{{.i18n.Tr "admin.users.max_repo_creation"}}</label> | |||
| <input id="max_repo_creation" name="max_repo_creation" type="number" value="{{.Org.MaxRepoCreation}}"> | |||
| <p class="help">{{.i18n.Tr "admin.users.max_repo_creation_desc"}}</p> | |||
| </div> | |||
| {{end}} | |||
| <div class="field"> | |||
| <button class="ui green button">{{$.i18n.Tr "org.settings.update_settings"}}</button> | |||
| </div> | |||
| </form> | |||
| <div class="field"> | |||
| <button class="ui green button">{{$.i18n.Tr "org.settings.update_settings"}}</button> | |||
| </div> | |||
| </form> | |||
| <div class="ui divider"></div> | |||
| <div class="ui divider"></div> | |||
| <form class="ui form" action="{{.Link}}/avatar" method="post" enctype="multipart/form-data"> | |||
| {{.CsrfTokenHtml}} | |||
| <div class="inline field"> | |||
| <label for="avatar">{{.i18n.Tr "settings.choose_new_avatar"}}</label> | |||
| <input name="avatar" type="file" > | |||
| </div> | |||
| <form class="ui form" action="{{.Link}}/avatar" method="post" enctype="multipart/form-data"> | |||
| {{.CsrfTokenHtml}} | |||
| <div class="inline field"> | |||
| <label for="avatar">{{.i18n.Tr "settings.choose_new_avatar"}}</label> | |||
| <input name="avatar" type="file" > | |||
| </div> | |||
| <div class="field"> | |||
| <button class="ui green button">{{$.i18n.Tr "settings.update_avatar"}}</button> | |||
| <a class="ui red button delete-post" data-request-url="{{.Link}}/avatar/delete" data-done-url="{{.Link}}">{{$.i18n.Tr "settings.delete_current_avatar"}}</a> | |||
| </div> | |||
| </form> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div class="field"> | |||
| <button class="ui green button">{{$.i18n.Tr "settings.update_avatar"}}</button> | |||
| <a class="ui red button delete-post" data-request-url="{{.Link}}/avatar/delete" data-done-url="{{.Link}}">{{$.i18n.Tr "settings.delete_current_avatar"}}</a> | |||
| </div> | |||
| </form> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| {{template "base/footer" .}} | |||
| @@ -0,0 +1,85 @@ | |||
| {{template "base/head" .}} | |||
| <div class="repository release dataset-list view"> | |||
| {{template "repo/header" .}} | |||
| <div class="ui container"> | |||
| <div class="ui three column stackable grid"> | |||
| <div class="column"> | |||
| <h2>{{.i18n.Tr "cloudbrain"}}</h2> | |||
| </div> | |||
| <div class="column"> | |||
| </div> | |||
| <div class="column right aligned"> | |||
| <a class="ui green button" href="{{.RepoLink}}/cloudbrain/create">{{.i18n.Tr "repo.cloudbrain.new"}}</a> | |||
| </div> | |||
| </div> | |||
| <div class="ui divider"></div> | |||
| <div class="ui grid"> | |||
| <div class="row"> | |||
| <div class="ui sixteen wide column"> | |||
| <div class="ui sixteen wide column"> | |||
| <div class="ui two column stackable grid"> | |||
| <div class="column"> | |||
| </div> | |||
| <div class="column right aligned"> | |||
| <div class="ui right dropdown type jump item"> | |||
| <span class="text"> | |||
| {{.i18n.Tr "repo.issues.filter_sort"}}<i class="dropdown icon"></i> | |||
| </span> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div class="dataset list"> | |||
| {{range .Tasks}} | |||
| <div class="ui grid item"> | |||
| <div class="row"> | |||
| <div class="seven wide column"> | |||
| <a class="title" href="{{$.Link}}/{{.JobID}}"> | |||
| <span class="fitted">{{svg "octicon-tasklist" 16}}</span> | |||
| <span class="fitted">{{.JobName}}</span> | |||
| </a> | |||
| </div> | |||
| <div class="four wide column job-status" id="{{.JobID}}" data-repopath="{{$.RepoRelPath}}" data-jobid="{{.JobID}}"> | |||
| {{.Status}} | |||
| </div> | |||
| <div class="three wide column"> | |||
| <span class="ui text center">{{svg "octicon-flame" 16}} {{TimeSinceUnix .CreatedUnix $.Lang}}</span> | |||
| </div> | |||
| <div class="two wide column"> | |||
| <span class="ui text center clipboard"> | |||
| <a class="title" href="{{$.Link}}/{{.JobID}}"> | |||
| <span class="fitted">查看</span> | |||
| </a> | |||
| </span> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| {{end}} | |||
| {{template "base/paginate" .}} | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| {{template "base/footer" .}} | |||
| <script> | |||
| $( document ).ready(function() { | |||
| $( ".job-status" ).each(( index, job ) => { | |||
| const jobID = job.dataset.jobid; | |||
| const repoPath = job.dataset.repopath; | |||
| if (job.textContent.trim() != 'WAITING') { | |||
| return | |||
| } | |||
| $.get( `/api/v1/repos/${repoPath}/cloudbrain/${jobID}`, ( data ) => { | |||
| const jobID = data.JobID | |||
| const status = data.JobStatus | |||
| $('#'+ jobID).text(status) | |||
| // console.log(data) | |||
| }).fail(function(err) { | |||
| console.log( err ); | |||
| }); | |||
| }); | |||
| }); | |||
| </script> | |||
| @@ -0,0 +1,39 @@ | |||
| {{template "base/head" .}} | |||
| <div class="repository new repo"> | |||
| {{template "repo/header" .}} | |||
| <div class="ui middle very relaxed page grid"> | |||
| <div class="column"> | |||
| {{template "base/alert" .}} | |||
| <form class="ui form" action="{{.Link}}" method="post"> | |||
| {{.CsrfTokenHtml}} | |||
| <h3 class="ui top attached header"> | |||
| New Cloudbrain task | |||
| </h3> | |||
| <div class="ui attached segment"> | |||
| <br> | |||
| <div class="inline required field"> | |||
| <label>任务名称</label> | |||
| <input name="job_name" id="cloudbrain_job_name" placeholder="任务名称" value="{{.job_name}}" tabindex="3" autofocus required maxlength="255"> | |||
| </div> | |||
| <br> | |||
| <div class="inline required field"> | |||
| <label>镜像</label> | |||
| <input name="image" id="cloudbrain_image" placeholder="输入镜像" value="{{.image}}" tabindex="3" autofocus required maxlength="255"> | |||
| </div> | |||
| <div class="inline required field"> | |||
| <label>启动命令</label> | |||
| <textarea name="command" rows="10">{{.command}}</textarea> | |||
| </div> | |||
| <div class="inline field"> | |||
| <label></label> | |||
| <button class="ui green button"> | |||
| Create Cloudbrain | |||
| </button> | |||
| <a class="ui button" href="/">Cancel</a> | |||
| </div> | |||
| </div> | |||
| </form> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| {{template "base/footer" .}} | |||
| @@ -0,0 +1,104 @@ | |||
| {{template "base/head" .}} | |||
| <div class="repository new repo"> | |||
| {{template "repo/header" .}} | |||
| <div class="ui middle very relaxed page grid"> | |||
| <div class="column"> | |||
| {{template "base/alert" .}} | |||
| <h4 class="ui header" id="vertical-segment"> | |||
| <a href="javascript:window.history.back();"><i class="arrow left icon"></i>返回</a> | |||
| </h4> | |||
| <div> | |||
| <div class="ui yellow segment"> | |||
| {{with .task}} | |||
| <p>任务名称: {{.JobName}}</p> | |||
| {{end}} | |||
| </div> | |||
| <div class="ui green segment"> | |||
| <p>任务结果:</p> | |||
| {{with .taskRes}} | |||
| {{range .TaskStatuses}} | |||
| <table class="ui celled striped table"> | |||
| <tbody> | |||
| <tr> | |||
| <td class="four wide"> 状态 </td> | |||
| <td> {{.State}} </td> | |||
| </tr> | |||
| <tr> | |||
| <td> 开始时间 </td> | |||
| <td>{{.StartAt}}</td> | |||
| </tr> | |||
| <tr> | |||
| <td> 结束时间 </td> | |||
| <td>{{.FinishedAt}}</td> | |||
| </tr> | |||
| <tr> | |||
| <td> ExitCode </td> | |||
| <td>{{.ExitCode}}</td> | |||
| </tr> | |||
| <tr> | |||
| <td> 退出信息 </td> | |||
| <td>{{.ExitDiagnostics| nl2br}}</td> | |||
| </tr> | |||
| </tbody> | |||
| </table> | |||
| {{end}} | |||
| {{end}} | |||
| </div> | |||
| <div class="ui blue segment"> | |||
| {{with .result}} | |||
| <table class="ui celled striped table"> | |||
| <thead> | |||
| <tr> <th colspan="2"> 硬件信息 </th> </tr> | |||
| </thead> | |||
| <tbody> | |||
| <tr> | |||
| <td class="four wide"> CPU </td> | |||
| <td>{{.Resource.CPU}}</td> | |||
| </tr> | |||
| <tr> | |||
| <td> Memory </td> | |||
| <td>{{.Resource.Memory}}</td> | |||
| </tr> | |||
| <tr> | |||
| <td> NvidiaComGpu </td> | |||
| <td>{{.Resource.NvidiaComGpu}}</td> | |||
| </tr> | |||
| </tbody> | |||
| </table> | |||
| <table class="ui celled striped table"> | |||
| <thead> | |||
| <tr> <th colspan="2"> 调试信息 </th> </tr> | |||
| </thead> | |||
| <tbody> | |||
| <tr> | |||
| <td class="four wide"> 状态 </td> | |||
| <td> {{.Platform}} </td> | |||
| </tr> | |||
| <tr> | |||
| <td> 开始时间 </td> | |||
| <td>{{.JobStatus.CreatedTime}}</td> | |||
| </tr> | |||
| <tr> | |||
| <td> 结束时间 </td> | |||
| <td>{{.JobStatus.CompletedTime}}</td> | |||
| </tr> | |||
| <tr> | |||
| <td> ExitCode </td> | |||
| <td>{{.JobStatus.AppExitCode}}</td> | |||
| </tr> | |||
| <tr> | |||
| <td> 退出信息 </td> | |||
| <td>{{.JobStatus.AppExitDiagnostics | nl2br}}</td> | |||
| </tr> | |||
| </tbody> | |||
| </table> | |||
| {{end}} | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| {{template "base/footer" .}} | |||
| @@ -139,6 +139,12 @@ | |||
| </a> | |||
| {{end}} | |||
| {{if .Permission.CanRead $.UnitTypeCloudBrain}} | |||
| <a class="{{if .PageIsCloudBrain}}active{{end}} item" href="{{.RepoLink}}/cloudbrain"> | |||
| {{svg "octicon-file-submodule" 16}} {{.i18n.Tr "cloudbrains"}} | |||
| </a> | |||
| {{end}} | |||
| {{template "custom/extra_tabs" .}} | |||
| {{if .Permission.IsAdmin}} | |||
| @@ -150,6 +150,15 @@ | |||
| </div> | |||
| </div> | |||
| {{$isCloudBrainEnabled := .Repository.UnitEnabled $.UnitTypeCloudBrain }} | |||
| <div class="inline field"> | |||
| <label>{{.i18n.Tr "repo.cloudbrain"}}</label> | |||
| <div class="ui checkbox"> | |||
| <input class="enable-system" name="enable_cloud_brain" type="checkbox" {{if $isCloudBrainEnabled}}checked{{end}}> | |||
| <label>{{.i18n.Tr "repo.settings.cloudbrain_desc"}}</label> | |||
| </div> | |||
| </div> | |||
| {{$isWikiEnabled := or (.Repository.UnitEnabled $.UnitTypeWiki) (.Repository.UnitEnabled $.UnitTypeExternalWiki)}} | |||
| <div class="inline field"> | |||
| <label>{{.i18n.Tr "repo.wiki"}}</label> | |||
| @@ -2324,6 +2324,44 @@ | |||
| } | |||
| } | |||
| }, | |||
| "/repos/{owner}/{repo}/cloudbrain/{jobid}": { | |||
| "get": { | |||
| "produces": [ | |||
| "application/json" | |||
| ], | |||
| "tags": [ | |||
| "cloudbrain" | |||
| ], | |||
| "summary": "Get a single task", | |||
| "operationId": "jobTask", | |||
| "parameters": [ | |||
| { | |||
| "type": "string", | |||
| "description": "owner of the repo", | |||
| "name": "owner", | |||
| "in": "path", | |||
| "required": true | |||
| }, | |||
| { | |||
| "type": "string", | |||
| "description": "name of the repo", | |||
| "name": "repo", | |||
| "in": "path", | |||
| "required": true | |||
| }, | |||
| { | |||
| "type": "string", | |||
| "description": "id of cloudbrain jobid", | |||
| "name": "jobid", | |||
| "in": "path", | |||
| "required": true | |||
| } | |||
| ], | |||
| "responses": { | |||
| "200": {} | |||
| } | |||
| } | |||
| }, | |||
| "/repos/{owner}/{repo}/collaborators": { | |||
| "get": { | |||
| "produces": [ | |||
| @@ -0,0 +1,31 @@ | |||
| # Compiled Object files, Static and Dynamic libs (Shared Objects) | |||
| *.o | |||
| *.a | |||
| *.so | |||
| # Folders | |||
| _obj | |||
| _test | |||
| # Architecture specific extensions/prefixes | |||
| *.[568vq] | |||
| [568vq].out | |||
| *.cgo1.go | |||
| *.cgo2.c | |||
| _cgo_defun.c | |||
| _cgo_gotypes.go | |||
| _cgo_export.* | |||
| _testmain.go | |||
| *.exe | |||
| *.test | |||
| *.prof | |||
| coverage.out | |||
| coverage.txt | |||
| go.sum | |||
| # Exclude intellij IDE folders | |||
| .idea/* | |||
| @@ -0,0 +1,21 @@ | |||
| language: go | |||
| sudo: false | |||
| go: # use travis ci resource effectively, keep always latest 2 versions and tip :) | |||
| - 1.14.x | |||
| - 1.13.x | |||
| - tip | |||
| install: | |||
| - go get -v -t ./... | |||
| script: | |||
| - go test ./... -race -coverprofile=coverage.txt -covermode=atomic | |||
| after_success: | |||
| - bash <(curl -s https://codecov.io/bash) | |||
| matrix: | |||
| allow_failures: | |||
| - go: tip | |||
| @@ -0,0 +1,36 @@ | |||
| package(default_visibility = ["//visibility:private"]) | |||
| load("@bazel_gazelle//:def.bzl", "gazelle") | |||
| load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") | |||
| gazelle( | |||
| name = "gazelle", | |||
| command = "fix", | |||
| prefix = "github.com/go-resty/resty/v2", | |||
| ) | |||
| go_library( | |||
| name = "go_default_library", | |||
| srcs = glob( | |||
| ["*.go"], | |||
| exclude = ["*_test.go"], | |||
| ), | |||
| importpath = "github.com/go-resty/resty/v2", | |||
| visibility = ["//visibility:public"], | |||
| deps = ["@org_golang_x_net//publicsuffix:go_default_library"], | |||
| ) | |||
| go_test( | |||
| name = "go_default_test", | |||
| srcs = | |||
| glob( | |||
| ["*_test.go"], | |||
| exclude = ["example_test.go"], | |||
| ), | |||
| data = glob([".testdata/*"]), | |||
| embed = [":go_default_library"], | |||
| importpath = "github.com/go-resty/resty/v2", | |||
| deps = [ | |||
| "@org_golang_x_net//proxy:go_default_library", | |||
| ], | |||
| ) | |||
| @@ -0,0 +1,21 @@ | |||
| The MIT License (MIT) | |||
| Copyright (c) 2015-2020 Jeevanandam M., https://myjeeva.com <jeeva@myjeeva.com> | |||
| Permission is hereby granted, free of charge, to any person obtaining a copy | |||
| of this software and associated documentation files (the "Software"), to deal | |||
| in the Software without restriction, including without limitation the rights | |||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
| copies of the Software, and to permit persons to whom the Software is | |||
| furnished to do so, subject to the following conditions: | |||
| The above copyright notice and this permission notice shall be included in all | |||
| copies or substantial portions of the Software. | |||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||
| SOFTWARE. | |||
| @@ -0,0 +1,850 @@ | |||
| <p align="center"> | |||
| <h1 align="center">Resty</h1> | |||
| <p align="center">Simple HTTP and REST client library for Go (inspired by Ruby rest-client)</p> | |||
| <p align="center"><a href="#features">Features</a> section describes in detail about Resty capabilities</p> | |||
| </p> | |||
| <p align="center"> | |||
| <p align="center"><a href="https://travis-ci.org/go-resty/resty"><img src="https://travis-ci.org/go-resty/resty.svg?branch=master" alt="Build Status"></a> <a href="https://codecov.io/gh/go-resty/resty/branch/master"><img src="https://codecov.io/gh/go-resty/resty/branch/master/graph/badge.svg" alt="Code Coverage"></a> <a href="https://goreportcard.com/report/go-resty/resty"><img src="https://goreportcard.com/badge/go-resty/resty" alt="Go Report Card"></a> <a href="https://github.com/go-resty/resty/releases/latest"><img src="https://img.shields.io/badge/version-2.3.0-blue.svg" alt="Release Version"></a> <a href="https://pkg.go.dev/github.com/go-resty/resty/v2"><img src="https://godoc.org/github.com/go-resty/resty?status.svg" alt="GoDoc"></a> <a href="LICENSE"><img src="https://img.shields.io/github/license/go-resty/resty.svg" alt="License"></a> <a href="https://github.com/avelino/awesome-go"><img src="https://awesome.re/mentioned-badge.svg" alt="Mentioned in Awesome Go"></a></p> | |||
| </p> | |||
| <p align="center"> | |||
| <h4 align="center">Resty Communication Channels</h4> | |||
| <p align="center"><a href="https://gitter.im/go_resty/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img src="https://badges.gitter.im/go_resty/community.svg" alt="Chat on Gitter - Resty Community"></a> <a href="https://twitter.com/go_resty"><img src="https://img.shields.io/badge/twitter-@go__resty-55acee.svg" alt="Twitter @go_resty"></a></p> | |||
| </p> | |||
| ## News | |||
| * v2.3.0 [released](https://github.com/go-resty/resty/releases/tag/v2.3.0) and tagged on May 20, 2020. | |||
| * v2.0.0 [released](https://github.com/go-resty/resty/releases/tag/v2.0.0) and tagged on Jul 16, 2019. | |||
| * v1.12.0 [released](https://github.com/go-resty/resty/releases/tag/v1.12.0) and tagged on Feb 27, 2019. | |||
| * v1.0 released and tagged on Sep 25, 2017. - Resty's first version was released on Sep 15, 2015 then it grew gradually as a very handy and helpful library. Its been a two years since first release. I'm very thankful to Resty users and its [contributors](https://github.com/go-resty/resty/graphs/contributors). | |||
| ## Features | |||
| * GET, POST, PUT, DELETE, HEAD, PATCH, OPTIONS, etc. | |||
| * Simple and chainable methods for settings and request | |||
| * [Request](https://godoc.org/github.com/go-resty/resty#Request) Body can be `string`, `[]byte`, `struct`, `map`, `slice` and `io.Reader` too | |||
| * Auto detects `Content-Type` | |||
| * Buffer less processing for `io.Reader` | |||
| * Request Body can be read multiple times via `Request.RawRequest.GetBody()` | |||
| * [Response](https://godoc.org/github.com/go-resty/resty#Response) object gives you more possibility | |||
| * Access as `[]byte` array - `response.Body()` OR Access as `string` - `response.String()` | |||
| * Know your `response.Time()` and when we `response.ReceivedAt()` | |||
| * Automatic marshal and unmarshal for `JSON` and `XML` content type | |||
| * Default is `JSON`, if you supply `struct/map` without header `Content-Type` | |||
| * For auto-unmarshal, refer to - | |||
| - Success scenario [Request.SetResult()](https://godoc.org/github.com/go-resty/resty#Request.SetResult) and [Response.Result()](https://godoc.org/github.com/go-resty/resty#Response.Result). | |||
| - Error scenario [Request.SetError()](https://godoc.org/github.com/go-resty/resty#Request.SetError) and [Response.Error()](https://godoc.org/github.com/go-resty/resty#Response.Error). | |||
| - Supports [RFC7807](https://tools.ietf.org/html/rfc7807) - `application/problem+json` & `application/problem+xml` | |||
| * Easy to upload one or more file(s) via `multipart/form-data` | |||
| * Auto detects file content type | |||
| * Request URL [Path Params (aka URI Params)](https://godoc.org/github.com/go-resty/resty#Request.SetPathParams) | |||
| * Backoff Retry Mechanism with retry condition function [reference](retry_test.go) | |||
| * Resty client HTTP & REST [Request](https://godoc.org/github.com/go-resty/resty#Client.OnBeforeRequest) and [Response](https://godoc.org/github.com/go-resty/resty#Client.OnAfterResponse) middlewares | |||
| * `Request.SetContext` supported | |||
| * Authorization option of `BasicAuth` and `Bearer` token | |||
| * Set request `ContentLength` value for all request or particular request | |||
| * Custom [Root Certificates](https://godoc.org/github.com/go-resty/resty#Client.SetRootCertificate) and Client [Certificates](https://godoc.org/github.com/go-resty/resty#Client.SetCertificates) | |||
| * Download/Save HTTP response directly into File, like `curl -o` flag. See [SetOutputDirectory](https://godoc.org/github.com/go-resty/resty#Client.SetOutputDirectory) & [SetOutput](https://godoc.org/github.com/go-resty/resty#Request.SetOutput). | |||
| * Cookies for your request and CookieJar support | |||
| * SRV Record based request instead of Host URL | |||
| * Client settings like `Timeout`, `RedirectPolicy`, `Proxy`, `TLSClientConfig`, `Transport`, etc. | |||
| * Optionally allows GET request with payload, see [SetAllowGetMethodPayload](https://godoc.org/github.com/go-resty/resty#Client.SetAllowGetMethodPayload) | |||
| * Supports registering external JSON library into resty, see [how to use](https://github.com/go-resty/resty/issues/76#issuecomment-314015250) | |||
| * Exposes Response reader without reading response (no auto-unmarshaling) if need be, see [how to use](https://github.com/go-resty/resty/issues/87#issuecomment-322100604) | |||
| * Option to specify expected `Content-Type` when response `Content-Type` header missing. Refer to [#92](https://github.com/go-resty/resty/issues/92) | |||
| * Resty design | |||
| * Have client level settings & options and also override at Request level if you want to | |||
| * Request and Response middlewares | |||
| * Create Multiple clients if you want to `resty.New()` | |||
| * Supports `http.RoundTripper` implementation, see [SetTransport](https://godoc.org/github.com/go-resty/resty#Client.SetTransport) | |||
| * goroutine concurrent safe | |||
| * Resty Client trace, see [Client.EnableTrace](https://godoc.org/github.com/go-resty/resty#Client.EnableTrace) and [Request.EnableTrace](https://godoc.org/github.com/go-resty/resty#Request.EnableTrace) | |||
| * Debug mode - clean and informative logging presentation | |||
| * Gzip - Go does it automatically also resty has fallback handling too | |||
| * Works fine with `HTTP/2` and `HTTP/1.1` | |||
| * [Bazel support](#bazel-support) | |||
| * Easily mock Resty for testing, [for e.g.](#mocking-http-requests-using-httpmock-library) | |||
| * Well tested client library | |||
| ### Included Batteries | |||
| * Redirect Policies - see [how to use](#redirect-policy) | |||
| * NoRedirectPolicy | |||
| * FlexibleRedirectPolicy | |||
| * DomainCheckRedirectPolicy | |||
| * etc. [more info](redirect.go) | |||
| * Retry Mechanism [how to use](#retries) | |||
| * Backoff Retry | |||
| * Conditional Retry | |||
| * SRV Record based request instead of Host URL [how to use](resty_test.go#L1412) | |||
| * etc (upcoming - throw your idea's [here](https://github.com/go-resty/resty/issues)). | |||
| #### Supported Go Versions | |||
| Initially Resty started supporting `go modules` since `v1.10.0` release. | |||
| Starting Resty v2 and higher versions, it fully embraces [go modules](https://github.com/golang/go/wiki/Modules) package release. It requires a Go version capable of understanding `/vN` suffixed imports: | |||
| - 1.9.7+ | |||
| - 1.10.3+ | |||
| - 1.11+ | |||
| ## It might be beneficial for your project :smile: | |||
| Resty author also published following projects for Go Community. | |||
| * [aah framework](https://aahframework.org) - A secure, flexible, rapid Go web framework. | |||
| * [THUMBAI](https://thumbai.app) - Go Mod Repository, Go Vanity Service and Simple Proxy Server. | |||
| * [go-model](https://github.com/jeevatkm/go-model) - Robust & Easy to use model mapper and utility methods for Go `struct`. | |||
| ## Installation | |||
| ```bash | |||
| # Go Modules | |||
| require github.com/go-resty/resty/v2 v2.3.0 | |||
| ``` | |||
| ## Usage | |||
| The following samples will assist you to become as comfortable as possible with resty library. | |||
| ```go | |||
| // Import resty into your code and refer it as `resty`. | |||
| import "github.com/go-resty/resty/v2" | |||
| ``` | |||
| #### Simple GET | |||
| ```go | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| resp, err := client.R(). | |||
| EnableTrace(). | |||
| Get("https://httpbin.org/get") | |||
| // Explore response object | |||
| fmt.Println("Response Info:") | |||
| fmt.Println("Error :", err) | |||
| fmt.Println("Status Code:", resp.StatusCode()) | |||
| fmt.Println("Status :", resp.Status()) | |||
| fmt.Println("Proto :", resp.Proto()) | |||
| fmt.Println("Time :", resp.Time()) | |||
| fmt.Println("Received At:", resp.ReceivedAt()) | |||
| fmt.Println("Body :\n", resp) | |||
| fmt.Println() | |||
| // Explore trace info | |||
| fmt.Println("Request Trace Info:") | |||
| ti := resp.Request.TraceInfo() | |||
| fmt.Println("DNSLookup :", ti.DNSLookup) | |||
| fmt.Println("ConnTime :", ti.ConnTime) | |||
| fmt.Println("TCPConnTime :", ti.TCPConnTime) | |||
| fmt.Println("TLSHandshake :", ti.TLSHandshake) | |||
| fmt.Println("ServerTime :", ti.ServerTime) | |||
| fmt.Println("ResponseTime :", ti.ResponseTime) | |||
| fmt.Println("TotalTime :", ti.TotalTime) | |||
| fmt.Println("IsConnReused :", ti.IsConnReused) | |||
| fmt.Println("IsConnWasIdle:", ti.IsConnWasIdle) | |||
| fmt.Println("ConnIdleTime :", ti.ConnIdleTime) | |||
| /* Output | |||
| Response Info: | |||
| Error : <nil> | |||
| Status Code: 200 | |||
| Status : 200 OK | |||
| Proto : HTTP/2.0 | |||
| Time : 475.611189ms | |||
| Received At: 2020-05-19 00:11:06.828188 -0700 PDT m=+0.476510773 | |||
| Body : | |||
| { | |||
| "args": {}, | |||
| "headers": { | |||
| "Accept-Encoding": "gzip", | |||
| "Host": "httpbin.org", | |||
| "User-Agent": "go-resty/2.3.0 (https://github.com/go-resty/resty)" | |||
| }, | |||
| "origin": "0.0.0.0", | |||
| "url": "https://httpbin.org/get" | |||
| } | |||
| Request Trace Info: | |||
| DNSLookup : 4.870246ms | |||
| ConnTime : 393.95373ms | |||
| TCPConnTime : 78.360432ms | |||
| TLSHandshake : 310.032859ms | |||
| ServerTime : 81.648284ms | |||
| ResponseTime : 124.266µs | |||
| TotalTime : 475.611189ms | |||
| IsConnReused : false | |||
| IsConnWasIdle: false | |||
| ConnIdleTime : 0s | |||
| */ | |||
| ``` | |||
| #### Enhanced GET | |||
| ```go | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| resp, err := client.R(). | |||
| SetQueryParams(map[string]string{ | |||
| "page_no": "1", | |||
| "limit": "20", | |||
| "sort":"name", | |||
| "order": "asc", | |||
| "random":strconv.FormatInt(time.Now().Unix(), 10), | |||
| }). | |||
| SetHeader("Accept", "application/json"). | |||
| SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F"). | |||
| Get("/search_result") | |||
| // Sample of using Request.SetQueryString method | |||
| resp, err := client.R(). | |||
| SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more"). | |||
| SetHeader("Accept", "application/json"). | |||
| SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F"). | |||
| Get("/show_product") | |||
| ``` | |||
| #### Various POST method combinations | |||
| ```go | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| // POST JSON string | |||
| // No need to set content type, if you have client level setting | |||
| resp, err := client.R(). | |||
| SetHeader("Content-Type", "application/json"). | |||
| SetBody(`{"username":"testuser", "password":"testpass"}`). | |||
| SetResult(&AuthSuccess{}). // or SetResult(AuthSuccess{}). | |||
| Post("https://myapp.com/login") | |||
| // POST []byte array | |||
| // No need to set content type, if you have client level setting | |||
| resp, err := client.R(). | |||
| SetHeader("Content-Type", "application/json"). | |||
| SetBody([]byte(`{"username":"testuser", "password":"testpass"}`)). | |||
| SetResult(&AuthSuccess{}). // or SetResult(AuthSuccess{}). | |||
| Post("https://myapp.com/login") | |||
| // POST Struct, default is JSON content type. No need to set one | |||
| resp, err := client.R(). | |||
| SetBody(User{Username: "testuser", Password: "testpass"}). | |||
| SetResult(&AuthSuccess{}). // or SetResult(AuthSuccess{}). | |||
| SetError(&AuthError{}). // or SetError(AuthError{}). | |||
| Post("https://myapp.com/login") | |||
| // POST Map, default is JSON content type. No need to set one | |||
| resp, err := client.R(). | |||
| SetBody(map[string]interface{}{"username": "testuser", "password": "testpass"}). | |||
| SetResult(&AuthSuccess{}). // or SetResult(AuthSuccess{}). | |||
| SetError(&AuthError{}). // or SetError(AuthError{}). | |||
| Post("https://myapp.com/login") | |||
| // POST of raw bytes for file upload. For example: upload file to Dropbox | |||
| fileBytes, _ := ioutil.ReadFile("/Users/jeeva/mydocument.pdf") | |||
| // See we are not setting content-type header, since go-resty automatically detects Content-Type for you | |||
| resp, err := client.R(). | |||
| SetBody(fileBytes). | |||
| SetContentLength(true). // Dropbox expects this value | |||
| SetAuthToken("<your-auth-token>"). | |||
| SetError(&DropboxError{}). // or SetError(DropboxError{}). | |||
| Post("https://content.dropboxapi.com/1/files_put/auto/resty/mydocument.pdf") // for upload Dropbox supports PUT too | |||
| // Note: resty detects Content-Type for request body/payload if content type header is not set. | |||
| // * For struct and map data type defaults to 'application/json' | |||
| // * Fallback is plain text content type | |||
| ``` | |||
| #### Sample PUT | |||
| You can use various combinations of `PUT` method call like demonstrated for `POST`. | |||
| ```go | |||
| // Note: This is one sample of PUT method usage, refer POST for more combination | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| // Request goes as JSON content type | |||
| // No need to set auth token, error, if you have client level settings | |||
| resp, err := client.R(). | |||
| SetBody(Article{ | |||
| Title: "go-resty", | |||
| Content: "This is my article content, oh ya!", | |||
| Author: "Jeevanandam M", | |||
| Tags: []string{"article", "sample", "resty"}, | |||
| }). | |||
| SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD"). | |||
| SetError(&Error{}). // or SetError(Error{}). | |||
| Put("https://myapp.com/article/1234") | |||
| ``` | |||
| #### Sample PATCH | |||
| You can use various combinations of `PATCH` method call like demonstrated for `POST`. | |||
| ```go | |||
| // Note: This is one sample of PUT method usage, refer POST for more combination | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| // Request goes as JSON content type | |||
| // No need to set auth token, error, if you have client level settings | |||
| resp, err := client.R(). | |||
| SetBody(Article{ | |||
| Tags: []string{"new tag1", "new tag2"}, | |||
| }). | |||
| SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD"). | |||
| SetError(&Error{}). // or SetError(Error{}). | |||
| Patch("https://myapp.com/articles/1234") | |||
| ``` | |||
| #### Sample DELETE, HEAD, OPTIONS | |||
| ```go | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| // DELETE a article | |||
| // No need to set auth token, error, if you have client level settings | |||
| resp, err := client.R(). | |||
| SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD"). | |||
| SetError(&Error{}). // or SetError(Error{}). | |||
| Delete("https://myapp.com/articles/1234") | |||
| // DELETE a articles with payload/body as a JSON string | |||
| // No need to set auth token, error, if you have client level settings | |||
| resp, err := client.R(). | |||
| SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD"). | |||
| SetError(&Error{}). // or SetError(Error{}). | |||
| SetHeader("Content-Type", "application/json"). | |||
| SetBody(`{article_ids: [1002, 1006, 1007, 87683, 45432] }`). | |||
| Delete("https://myapp.com/articles") | |||
| // HEAD of resource | |||
| // No need to set auth token, if you have client level settings | |||
| resp, err := client.R(). | |||
| SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD"). | |||
| Head("https://myapp.com/videos/hi-res-video") | |||
| // OPTIONS of resource | |||
| // No need to set auth token, if you have client level settings | |||
| resp, err := client.R(). | |||
| SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD"). | |||
| Options("https://myapp.com/servers/nyc-dc-01") | |||
| ``` | |||
| ### Multipart File(s) upload | |||
| #### Using io.Reader | |||
| ```go | |||
| profileImgBytes, _ := ioutil.ReadFile("/Users/jeeva/test-img.png") | |||
| notesBytes, _ := ioutil.ReadFile("/Users/jeeva/text-file.txt") | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| resp, err := client.R(). | |||
| SetFileReader("profile_img", "test-img.png", bytes.NewReader(profileImgBytes)). | |||
| SetFileReader("notes", "text-file.txt", bytes.NewReader(notesBytes)). | |||
| SetFormData(map[string]string{ | |||
| "first_name": "Jeevanandam", | |||
| "last_name": "M", | |||
| }). | |||
| Post("http://myapp.com/upload") | |||
| ``` | |||
| #### Using File directly from Path | |||
| ```go | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| // Single file scenario | |||
| resp, err := client.R(). | |||
| SetFile("profile_img", "/Users/jeeva/test-img.png"). | |||
| Post("http://myapp.com/upload") | |||
| // Multiple files scenario | |||
| resp, err := client.R(). | |||
| SetFiles(map[string]string{ | |||
| "profile_img": "/Users/jeeva/test-img.png", | |||
| "notes": "/Users/jeeva/text-file.txt", | |||
| }). | |||
| Post("http://myapp.com/upload") | |||
| // Multipart of form fields and files | |||
| resp, err := client.R(). | |||
| SetFiles(map[string]string{ | |||
| "profile_img": "/Users/jeeva/test-img.png", | |||
| "notes": "/Users/jeeva/text-file.txt", | |||
| }). | |||
| SetFormData(map[string]string{ | |||
| "first_name": "Jeevanandam", | |||
| "last_name": "M", | |||
| "zip_code": "00001", | |||
| "city": "my city", | |||
| "access_token": "C6A79608-782F-4ED0-A11D-BD82FAD829CD", | |||
| }). | |||
| Post("http://myapp.com/profile") | |||
| ``` | |||
| #### Sample Form submission | |||
| ```go | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| // just mentioning about POST as an example with simple flow | |||
| // User Login | |||
| resp, err := client.R(). | |||
| SetFormData(map[string]string{ | |||
| "username": "jeeva", | |||
| "password": "mypass", | |||
| }). | |||
| Post("http://myapp.com/login") | |||
| // Followed by profile update | |||
| resp, err := client.R(). | |||
| SetFormData(map[string]string{ | |||
| "first_name": "Jeevanandam", | |||
| "last_name": "M", | |||
| "zip_code": "00001", | |||
| "city": "new city update", | |||
| }). | |||
| Post("http://myapp.com/profile") | |||
| // Multi value form data | |||
| criteria := url.Values{ | |||
| "search_criteria": []string{"book", "glass", "pencil"}, | |||
| } | |||
| resp, err := client.R(). | |||
| SetFormDataFromValues(criteria). | |||
| Post("http://myapp.com/search") | |||
| ``` | |||
| #### Save HTTP Response into File | |||
| ```go | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| // Setting output directory path, If directory not exists then resty creates one! | |||
| // This is optional one, if you're planning using absoule path in | |||
| // `Request.SetOutput` and can used together. | |||
| client.SetOutputDirectory("/Users/jeeva/Downloads") | |||
| // HTTP response gets saved into file, similar to curl -o flag | |||
| _, err := client.R(). | |||
| SetOutput("plugin/ReplyWithHeader-v5.1-beta.zip"). | |||
| Get("http://bit.ly/1LouEKr") | |||
| // OR using absolute path | |||
| // Note: output directory path is not used for absolute path | |||
| _, err := client.R(). | |||
| SetOutput("/MyDownloads/plugin/ReplyWithHeader-v5.1-beta.zip"). | |||
| Get("http://bit.ly/1LouEKr") | |||
| ``` | |||
| #### Request URL Path Params | |||
| Resty provides easy to use dynamic request URL path params. Params can be set at client and request level. Client level params value can be overridden at request level. | |||
| ```go | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| client.R().SetPathParams(map[string]string{ | |||
| "userId": "sample@sample.com", | |||
| "subAccountId": "100002", | |||
| }). | |||
| Get("/v1/users/{userId}/{subAccountId}/details") | |||
| // Result: | |||
| // Composed URL - /v1/users/sample@sample.com/100002/details | |||
| ``` | |||
| #### Request and Response Middleware | |||
| Resty provides middleware ability to manipulate for Request and Response. It is more flexible than callback approach. | |||
| ```go | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| // Registering Request Middleware | |||
| client.OnBeforeRequest(func(c *resty.Client, req *resty.Request) error { | |||
| // Now you have access to Client and current Request object | |||
| // manipulate it as per your need | |||
| return nil // if its success otherwise return error | |||
| }) | |||
| // Registering Response Middleware | |||
| client.OnAfterResponse(func(c *resty.Client, resp *resty.Response) error { | |||
| // Now you have access to Client and current Response object | |||
| // manipulate it as per your need | |||
| return nil // if its success otherwise return error | |||
| }) | |||
| ``` | |||
| #### Redirect Policy | |||
| Resty provides few ready to use redirect policy(s) also it supports multiple policies together. | |||
| ```go | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| // Assign Client Redirect Policy. Create one as per you need | |||
| client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(15)) | |||
| // Wanna multiple policies such as redirect count, domain name check, etc | |||
| client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20), | |||
| resty.DomainCheckRedirectPolicy("host1.com", "host2.org", "host3.net")) | |||
| ``` | |||
| ##### Custom Redirect Policy | |||
| Implement [RedirectPolicy](redirect.go#L20) interface and register it with resty client. Have a look [redirect.go](redirect.go) for more information. | |||
| ```go | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| // Using raw func into resty.SetRedirectPolicy | |||
| client.SetRedirectPolicy(resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error { | |||
| // Implement your logic here | |||
| // return nil for continue redirect otherwise return error to stop/prevent redirect | |||
| return nil | |||
| })) | |||
| //--------------------------------------------------- | |||
| // Using struct create more flexible redirect policy | |||
| type CustomRedirectPolicy struct { | |||
| // variables goes here | |||
| } | |||
| func (c *CustomRedirectPolicy) Apply(req *http.Request, via []*http.Request) error { | |||
| // Implement your logic here | |||
| // return nil for continue redirect otherwise return error to stop/prevent redirect | |||
| return nil | |||
| } | |||
| // Registering in resty | |||
| client.SetRedirectPolicy(CustomRedirectPolicy{/* initialize variables */}) | |||
| ``` | |||
| #### Custom Root Certificates and Client Certificates | |||
| ```go | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| // Custom Root certificates, just supply .pem file. | |||
| // you can add one or more root certificates, its get appended | |||
| client.SetRootCertificate("/path/to/root/pemFile1.pem") | |||
| client.SetRootCertificate("/path/to/root/pemFile2.pem") | |||
| // ... and so on! | |||
| // Adding Client Certificates, you add one or more certificates | |||
| // Sample for creating certificate object | |||
| // Parsing public/private key pair from a pair of files. The files must contain PEM encoded data. | |||
| cert1, err := tls.LoadX509KeyPair("certs/client.pem", "certs/client.key") | |||
| if err != nil { | |||
| log.Fatalf("ERROR client certificate: %s", err) | |||
| } | |||
| // ... | |||
| // You add one or more certificates | |||
| client.SetCertificates(cert1, cert2, cert3) | |||
| ``` | |||
| #### Custom Root Certificates and Client Certificates from string | |||
| ```go | |||
| // Custom Root certificates from string | |||
| // You can pass you certificates throught env variables as strings | |||
| // you can add one or more root certificates, its get appended | |||
| client.SetRootCertificateFromString("-----BEGIN CERTIFICATE-----content-----END CERTIFICATE-----") | |||
| client.SetRootCertificateFromString("-----BEGIN CERTIFICATE-----content-----END CERTIFICATE-----") | |||
| // ... and so on! | |||
| // Adding Client Certificates, you add one or more certificates | |||
| // Sample for creating certificate object | |||
| // Parsing public/private key pair from a pair of files. The files must contain PEM encoded data. | |||
| cert1, err := tls.X509KeyPair([]byte("-----BEGIN CERTIFICATE-----content-----END CERTIFICATE-----"), []byte("-----BEGIN CERTIFICATE-----content-----END CERTIFICATE-----")) | |||
| if err != nil { | |||
| log.Fatalf("ERROR client certificate: %s", err) | |||
| } | |||
| // ... | |||
| // You add one or more certificates | |||
| client.SetCertificates(cert1, cert2, cert3) | |||
| ``` | |||
| #### Proxy Settings - Client as well as at Request Level | |||
| Default `Go` supports Proxy via environment variable `HTTP_PROXY`. Resty provides support via `SetProxy` & `RemoveProxy`. | |||
| Choose as per your need. | |||
| **Client Level Proxy** settings applied to all the request | |||
| ```go | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| // Setting a Proxy URL and Port | |||
| client.SetProxy("http://proxyserver:8888") | |||
| // Want to remove proxy setting | |||
| client.RemoveProxy() | |||
| ``` | |||
| #### Retries | |||
| Resty uses [backoff](http://www.awsarchitectureblog.com/2015/03/backoff.html) | |||
| to increase retry intervals after each attempt. | |||
| Usage example: | |||
| ```go | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| // Retries are configured per client | |||
| client. | |||
| // Set retry count to non zero to enable retries | |||
| SetRetryCount(3). | |||
| // You can override initial retry wait time. | |||
| // Default is 100 milliseconds. | |||
| SetRetryWaitTime(5 * time.Second). | |||
| // MaxWaitTime can be overridden as well. | |||
| // Default is 2 seconds. | |||
| SetRetryMaxWaitTime(20 * time.Second). | |||
| // SetRetryAfter sets callback to calculate wait time between retries. | |||
| // Default (nil) implies exponential backoff with jitter | |||
| SetRetryAfter(func(client *Client, resp *Response) (time.Duration, error) { | |||
| return 0, errors.New("quota exceeded") | |||
| }) | |||
| ``` | |||
| Above setup will result in resty retrying requests returned non nil error up to | |||
| 3 times with delay increased after each attempt. | |||
| You can optionally provide client with custom retry conditions: | |||
| ```go | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| client.AddRetryCondition( | |||
| // RetryConditionFunc type is for retry condition function | |||
| // input: non-nil Response OR request execution error | |||
| func(r *resty.Response, err error) bool { | |||
| return r.StatusCode() == http.StatusTooManyRequests | |||
| }, | |||
| ) | |||
| ``` | |||
| Above example will make resty retry requests ended with `429 Too Many Requests` | |||
| status code. | |||
| Multiple retry conditions can be added. | |||
| It is also possible to use `resty.Backoff(...)` to get arbitrary retry scenarios | |||
| implemented. [Reference](retry_test.go). | |||
| #### Allow GET request with Payload | |||
| ```go | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| // Allow GET request with Payload. This is disabled by default. | |||
| client.SetAllowGetMethodPayload(true) | |||
| ``` | |||
| #### Wanna Multiple Clients | |||
| ```go | |||
| // Here you go! | |||
| // Client 1 | |||
| client1 := resty.New() | |||
| client1.R().Get("http://httpbin.org") | |||
| // ... | |||
| // Client 2 | |||
| client2 := resty.New() | |||
| client2.R().Head("http://httpbin.org") | |||
| // ... | |||
| // Bend it as per your need!!! | |||
| ``` | |||
| #### Remaining Client Settings & its Options | |||
| ```go | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| // Unique settings at Client level | |||
| //-------------------------------- | |||
| // Enable debug mode | |||
| client.SetDebug(true) | |||
| // Assign Client TLSClientConfig | |||
| // One can set custom root-certificate. Refer: http://golang.org/pkg/crypto/tls/#example_Dial | |||
| client.SetTLSClientConfig(&tls.Config{ RootCAs: roots }) | |||
| // or One can disable security check (https) | |||
| client.SetTLSClientConfig(&tls.Config{ InsecureSkipVerify: true }) | |||
| // Set client timeout as per your need | |||
| client.SetTimeout(1 * time.Minute) | |||
| // You can override all below settings and options at request level if you want to | |||
| //-------------------------------------------------------------------------------- | |||
| // Host URL for all request. So you can use relative URL in the request | |||
| client.SetHostURL("http://httpbin.org") | |||
| // Headers for all request | |||
| client.SetHeader("Accept", "application/json") | |||
| client.SetHeaders(map[string]string{ | |||
| "Content-Type": "application/json", | |||
| "User-Agent": "My custom User Agent String", | |||
| }) | |||
| // Cookies for all request | |||
| client.SetCookie(&http.Cookie{ | |||
| Name:"go-resty", | |||
| Value:"This is cookie value", | |||
| Path: "/", | |||
| Domain: "sample.com", | |||
| MaxAge: 36000, | |||
| HttpOnly: true, | |||
| Secure: false, | |||
| }) | |||
| client.SetCookies(cookies) | |||
| // URL query parameters for all request | |||
| client.SetQueryParam("user_id", "00001") | |||
| client.SetQueryParams(map[string]string{ // sample of those who use this manner | |||
| "api_key": "api-key-here", | |||
| "api_secert": "api-secert", | |||
| }) | |||
| client.R().SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more") | |||
| // Form data for all request. Typically used with POST and PUT | |||
| client.SetFormData(map[string]string{ | |||
| "access_token": "BC594900-518B-4F7E-AC75-BD37F019E08F", | |||
| }) | |||
| // Basic Auth for all request | |||
| client.SetBasicAuth("myuser", "mypass") | |||
| // Bearer Auth Token for all request | |||
| client.SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F") | |||
| // Enabling Content length value for all request | |||
| client.SetContentLength(true) | |||
| // Registering global Error object structure for JSON/XML request | |||
| client.SetError(&Error{}) // or resty.SetError(Error{}) | |||
| ``` | |||
| #### Unix Socket | |||
| ```go | |||
| unixSocket := "/var/run/my_socket.sock" | |||
| // Create a Go's http.Transport so we can set it in resty. | |||
| transport := http.Transport{ | |||
| Dial: func(_, _ string) (net.Conn, error) { | |||
| return net.Dial("unix", unixSocket) | |||
| }, | |||
| } | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| // Set the previous transport that we created, set the scheme of the communication to the | |||
| // socket and set the unixSocket as the HostURL. | |||
| client.SetTransport(&transport).SetScheme("http").SetHostURL(unixSocket) | |||
| // No need to write the host's URL on the request, just the path. | |||
| client.R().Get("/index.html") | |||
| ``` | |||
| #### Bazel support | |||
| Resty can be built, tested and depended upon via [Bazel](https://bazel.build). | |||
| For example, to run all tests: | |||
| ```shell | |||
| bazel test :go_default_test | |||
| ``` | |||
| #### Mocking http requests using [httpmock](https://github.com/jarcoal/httpmock) library | |||
| In order to mock the http requests when testing your application you | |||
| could use the `httpmock` library. | |||
| When using the default resty client, you should pass the client to the library as follow: | |||
| ```go | |||
| // Create a Resty Client | |||
| client := resty.New() | |||
| // Get the underlying HTTP Client and set it to Mock | |||
| httpmock.ActivateNonDefault(client.GetClient()) | |||
| ``` | |||
| More detailed example of mocking resty http requests using ginko could be found [here](https://github.com/jarcoal/httpmock#ginkgo--resty-example). | |||
| ## Versioning | |||
| Resty releases versions according to [Semantic Versioning](http://semver.org) | |||
| * Resty v2 does not use `gopkg.in` service for library versioning. | |||
| * Resty fully adapted to `go mod` capabilities since `v1.10.0` release. | |||
| * Resty v1 series was using `gopkg.in` to provide versioning. `gopkg.in/resty.vX` points to appropriate tagged versions; `X` denotes version series number and it's a stable release for production use. For e.g. `gopkg.in/resty.v0`. | |||
| * Development takes place at the master branch. Although the code in master should always compile and test successfully, it might break API's. I aim to maintain backwards compatibility, but sometimes API's and behavior might be changed to fix a bug. | |||
| ## Contribution | |||
| I would welcome your contribution! If you find any improvement or issue you want to fix, feel free to send a pull request, I like pull requests that include test cases for fix/enhancement. I have done my best to bring pretty good code coverage. Feel free to write tests. | |||
| BTW, I'd like to know what you think about `Resty`. Kindly open an issue or send me an email; it'd mean a lot to me. | |||
| ## Creator | |||
| [Jeevanandam M.](https://github.com/jeevatkm) (jeeva@myjeeva.com) | |||
| ## Core Team | |||
| Have a look on [Members](https://github.com/orgs/go-resty/teams/core/members) page. | |||
| ## Contributors | |||
| Have a look on [Contributors](https://github.com/go-resty/resty/graphs/contributors) page. | |||
| ## License | |||
| Resty released under MIT license, refer [LICENSE](LICENSE) file. | |||
| @@ -0,0 +1,27 @@ | |||
| workspace(name = "resty") | |||
| git_repository( | |||
| name = "io_bazel_rules_go", | |||
| remote = "https://github.com/bazelbuild/rules_go.git", | |||
| tag = "0.13.0", | |||
| ) | |||
| git_repository( | |||
| name = "bazel_gazelle", | |||
| remote = "https://github.com/bazelbuild/bazel-gazelle.git", | |||
| tag = "0.13.0", | |||
| ) | |||
| load( | |||
| "@io_bazel_rules_go//go:def.bzl", | |||
| "go_rules_dependencies", | |||
| "go_register_toolchains", | |||
| ) | |||
| go_rules_dependencies() | |||
| go_register_toolchains() | |||
| load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies") | |||
| gazelle_dependencies() | |||
| @@ -0,0 +1,978 @@ | |||
| // Copyright (c) 2015-2020 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. | |||
| // resty source code and usage is governed by a MIT style | |||
| // license that can be found in the LICENSE file. | |||
| package resty | |||
| import ( | |||
| "bytes" | |||
| "compress/gzip" | |||
| "crypto/tls" | |||
| "crypto/x509" | |||
| "encoding/json" | |||
| "errors" | |||
| "fmt" | |||
| "io" | |||
| "io/ioutil" | |||
| "math" | |||
| "net/http" | |||
| "net/url" | |||
| "reflect" | |||
| "regexp" | |||
| "strings" | |||
| "sync" | |||
| "time" | |||
| ) | |||
| const ( | |||
| // MethodGet HTTP method | |||
| MethodGet = "GET" | |||
| // MethodPost HTTP method | |||
| MethodPost = "POST" | |||
| // MethodPut HTTP method | |||
| MethodPut = "PUT" | |||
| // MethodDelete HTTP method | |||
| MethodDelete = "DELETE" | |||
| // MethodPatch HTTP method | |||
| MethodPatch = "PATCH" | |||
| // MethodHead HTTP method | |||
| MethodHead = "HEAD" | |||
| // MethodOptions HTTP method | |||
| MethodOptions = "OPTIONS" | |||
| ) | |||
| var ( | |||
| hdrUserAgentKey = http.CanonicalHeaderKey("User-Agent") | |||
| hdrAcceptKey = http.CanonicalHeaderKey("Accept") | |||
| hdrContentTypeKey = http.CanonicalHeaderKey("Content-Type") | |||
| hdrContentLengthKey = http.CanonicalHeaderKey("Content-Length") | |||
| hdrContentEncodingKey = http.CanonicalHeaderKey("Content-Encoding") | |||
| hdrAuthorizationKey = http.CanonicalHeaderKey("Authorization") | |||
| plainTextType = "text/plain; charset=utf-8" | |||
| jsonContentType = "application/json" | |||
| formContentType = "application/x-www-form-urlencoded" | |||
| jsonCheck = regexp.MustCompile(`(?i:(application|text)/(json|.*\+json|json\-.*)(;|$))`) | |||
| xmlCheck = regexp.MustCompile(`(?i:(application|text)/(xml|.*\+xml)(;|$))`) | |||
| hdrUserAgentValue = "go-resty/" + Version + " (https://github.com/go-resty/resty)" | |||
| bufPool = &sync.Pool{New: func() interface{} { return &bytes.Buffer{} }} | |||
| ) | |||
| type ( | |||
| // RequestMiddleware type is for request middleware, called before a request is sent | |||
| RequestMiddleware func(*Client, *Request) error | |||
| // ResponseMiddleware type is for response middleware, called after a response has been received | |||
| ResponseMiddleware func(*Client, *Response) error | |||
| // PreRequestHook type is for the request hook, called right before the request is sent | |||
| PreRequestHook func(*Client, *http.Request) error | |||
| // RequestLogCallback type is for request logs, called before the request is logged | |||
| RequestLogCallback func(*RequestLog) error | |||
| // ResponseLogCallback type is for response logs, called before the response is logged | |||
| ResponseLogCallback func(*ResponseLog) error | |||
| ) | |||
| // Client struct is used to create Resty client with client level settings, | |||
| // these settings are applicable to all the request raised from the client. | |||
| // | |||
| // Resty also provides an options to override most of the client settings | |||
| // at request level. | |||
| type Client struct { | |||
| HostURL string | |||
| QueryParam url.Values | |||
| FormData url.Values | |||
| Header http.Header | |||
| UserInfo *User | |||
| Token string | |||
| AuthScheme string | |||
| Cookies []*http.Cookie | |||
| Error reflect.Type | |||
| Debug bool | |||
| DisableWarn bool | |||
| AllowGetMethodPayload bool | |||
| RetryCount int | |||
| RetryWaitTime time.Duration | |||
| RetryMaxWaitTime time.Duration | |||
| RetryConditions []RetryConditionFunc | |||
| RetryAfter RetryAfterFunc | |||
| JSONMarshal func(v interface{}) ([]byte, error) | |||
| JSONUnmarshal func(data []byte, v interface{}) error | |||
| jsonEscapeHTML bool | |||
| setContentLength bool | |||
| closeConnection bool | |||
| notParseResponse bool | |||
| trace bool | |||
| debugBodySizeLimit int64 | |||
| outputDirectory string | |||
| scheme string | |||
| pathParams map[string]string | |||
| log Logger | |||
| httpClient *http.Client | |||
| proxyURL *url.URL | |||
| beforeRequest []RequestMiddleware | |||
| udBeforeRequest []RequestMiddleware | |||
| preReqHook PreRequestHook | |||
| afterResponse []ResponseMiddleware | |||
| requestLog RequestLogCallback | |||
| responseLog ResponseLogCallback | |||
| } | |||
| // User type is to hold an username and password information | |||
| type User struct { | |||
| Username, Password string | |||
| } | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // Client methods | |||
| //___________________________________ | |||
| // SetHostURL method is to set Host URL in the client instance. It will be used with request | |||
| // raised from this client with relative URL | |||
| // // Setting HTTP address | |||
| // client.SetHostURL("http://myjeeva.com") | |||
| // | |||
| // // Setting HTTPS address | |||
| // client.SetHostURL("https://myjeeva.com") | |||
| func (c *Client) SetHostURL(url string) *Client { | |||
| c.HostURL = strings.TrimRight(url, "/") | |||
| return c | |||
| } | |||
| // SetHeader method sets a single header field and its value in the client instance. | |||
| // These headers will be applied to all requests raised from this client instance. | |||
| // Also it can be overridden at request level header options. | |||
| // | |||
| // See `Request.SetHeader` or `Request.SetHeaders`. | |||
| // | |||
| // For Example: To set `Content-Type` and `Accept` as `application/json` | |||
| // | |||
| // client. | |||
| // SetHeader("Content-Type", "application/json"). | |||
| // SetHeader("Accept", "application/json") | |||
| func (c *Client) SetHeader(header, value string) *Client { | |||
| c.Header.Set(header, value) | |||
| return c | |||
| } | |||
| // SetHeaders method sets multiple headers field and its values at one go in the client instance. | |||
| // These headers will be applied to all requests raised from this client instance. Also it can be | |||
| // overridden at request level headers options. | |||
| // | |||
| // See `Request.SetHeaders` or `Request.SetHeader`. | |||
| // | |||
| // For Example: To set `Content-Type` and `Accept` as `application/json` | |||
| // | |||
| // client.SetHeaders(map[string]string{ | |||
| // "Content-Type": "application/json", | |||
| // "Accept": "application/json", | |||
| // }) | |||
| func (c *Client) SetHeaders(headers map[string]string) *Client { | |||
| for h, v := range headers { | |||
| c.Header.Set(h, v) | |||
| } | |||
| return c | |||
| } | |||
| // SetCookieJar method sets custom http.CookieJar in the resty client. Its way to override default. | |||
| // | |||
| // For Example: sometimes we don't want to save cookies in api contacting, we can remove the default | |||
| // CookieJar in resty client. | |||
| // | |||
| // client.SetCookieJar(nil) | |||
| func (c *Client) SetCookieJar(jar http.CookieJar) *Client { | |||
| c.httpClient.Jar = jar | |||
| return c | |||
| } | |||
| // SetCookie method appends a single cookie in the client instance. | |||
| // These cookies will be added to all the request raised from this client instance. | |||
| // client.SetCookie(&http.Cookie{ | |||
| // Name:"go-resty", | |||
| // Value:"This is cookie value", | |||
| // }) | |||
| func (c *Client) SetCookie(hc *http.Cookie) *Client { | |||
| c.Cookies = append(c.Cookies, hc) | |||
| return c | |||
| } | |||
| // SetCookies method sets an array of cookies in the client instance. | |||
| // These cookies will be added to all the request raised from this client instance. | |||
| // cookies := []*http.Cookie{ | |||
| // &http.Cookie{ | |||
| // Name:"go-resty-1", | |||
| // Value:"This is cookie 1 value", | |||
| // }, | |||
| // &http.Cookie{ | |||
| // Name:"go-resty-2", | |||
| // Value:"This is cookie 2 value", | |||
| // }, | |||
| // } | |||
| // | |||
| // // Setting a cookies into resty | |||
| // client.SetCookies(cookies) | |||
| func (c *Client) SetCookies(cs []*http.Cookie) *Client { | |||
| c.Cookies = append(c.Cookies, cs...) | |||
| return c | |||
| } | |||
| // SetQueryParam method sets single parameter and its value in the client instance. | |||
| // It will be formed as query string for the request. | |||
| // | |||
| // For Example: `search=kitchen%20papers&size=large` | |||
| // in the URL after `?` mark. These query params will be added to all the request raised from | |||
| // this client instance. Also it can be overridden at request level Query Param options. | |||
| // | |||
| // See `Request.SetQueryParam` or `Request.SetQueryParams`. | |||
| // client. | |||
| // SetQueryParam("search", "kitchen papers"). | |||
| // SetQueryParam("size", "large") | |||
| func (c *Client) SetQueryParam(param, value string) *Client { | |||
| c.QueryParam.Set(param, value) | |||
| return c | |||
| } | |||
| // SetQueryParams method sets multiple parameters and their values at one go in the client instance. | |||
| // It will be formed as query string for the request. | |||
| // | |||
| // For Example: `search=kitchen%20papers&size=large` | |||
| // in the URL after `?` mark. These query params will be added to all the request raised from this | |||
| // client instance. Also it can be overridden at request level Query Param options. | |||
| // | |||
| // See `Request.SetQueryParams` or `Request.SetQueryParam`. | |||
| // client.SetQueryParams(map[string]string{ | |||
| // "search": "kitchen papers", | |||
| // "size": "large", | |||
| // }) | |||
| func (c *Client) SetQueryParams(params map[string]string) *Client { | |||
| for p, v := range params { | |||
| c.SetQueryParam(p, v) | |||
| } | |||
| return c | |||
| } | |||
| // SetFormData method sets Form parameters and their values in the client instance. | |||
| // It's applicable only HTTP method `POST` and `PUT` and requets content type would be set as | |||
| // `application/x-www-form-urlencoded`. These form data will be added to all the request raised from | |||
| // this client instance. Also it can be overridden at request level form data. | |||
| // | |||
| // See `Request.SetFormData`. | |||
| // client.SetFormData(map[string]string{ | |||
| // "access_token": "BC594900-518B-4F7E-AC75-BD37F019E08F", | |||
| // "user_id": "3455454545", | |||
| // }) | |||
| func (c *Client) SetFormData(data map[string]string) *Client { | |||
| for k, v := range data { | |||
| c.FormData.Set(k, v) | |||
| } | |||
| return c | |||
| } | |||
| // SetBasicAuth method sets the basic authentication header in the HTTP request. For Example: | |||
| // Authorization: Basic <base64-encoded-value> | |||
| // | |||
| // For Example: To set the header for username "go-resty" and password "welcome" | |||
| // client.SetBasicAuth("go-resty", "welcome") | |||
| // | |||
| // This basic auth information gets added to all the request rasied from this client instance. | |||
| // Also it can be overridden or set one at the request level is supported. | |||
| // | |||
| // See `Request.SetBasicAuth`. | |||
| func (c *Client) SetBasicAuth(username, password string) *Client { | |||
| c.UserInfo = &User{Username: username, Password: password} | |||
| return c | |||
| } | |||
| // SetAuthToken method sets the auth token of the `Authorization` header for all HTTP requests. | |||
| // The default auth scheme is `Bearer`, it can be customized with the method `SetAuthScheme`. For Example: | |||
| // Authorization: <auth-scheme> <auth-token-value> | |||
| // | |||
| // For Example: To set auth token BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F | |||
| // | |||
| // client.SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F") | |||
| // | |||
| // This auth token gets added to all the requests rasied from this client instance. | |||
| // Also it can be overridden or set one at the request level is supported. | |||
| // | |||
| // See `Request.SetAuthToken`. | |||
| func (c *Client) SetAuthToken(token string) *Client { | |||
| c.Token = token | |||
| return c | |||
| } | |||
| // SetAuthScheme method sets the auth scheme type in the HTTP request. For Example: | |||
| // Authorization: <auth-scheme-value> <auth-token-value> | |||
| // | |||
| // For Example: To set the scheme to use OAuth | |||
| // | |||
| // client.SetAuthScheme("OAuth") | |||
| // | |||
| // This auth scheme gets added to all the requests rasied from this client instance. | |||
| // Also it can be overridden or set one at the request level is supported. | |||
| // | |||
| // Information about auth schemes can be found in RFC7235 which is linked to below | |||
| // along with the page containing the currently defined official authentication schemes: | |||
| // https://tools.ietf.org/html/rfc7235 | |||
| // https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml#authschemes | |||
| // | |||
| // See `Request.SetAuthToken`. | |||
| func (c *Client) SetAuthScheme(scheme string) *Client { | |||
| c.AuthScheme = scheme | |||
| return c | |||
| } | |||
| // R method creates a new request instance, its used for Get, Post, Put, Delete, Patch, Head, Options, etc. | |||
| func (c *Client) R() *Request { | |||
| r := &Request{ | |||
| QueryParam: url.Values{}, | |||
| FormData: url.Values{}, | |||
| Header: http.Header{}, | |||
| Cookies: make([]*http.Cookie, 0), | |||
| client: c, | |||
| multipartFiles: []*File{}, | |||
| multipartFields: []*MultipartField{}, | |||
| pathParams: map[string]string{}, | |||
| jsonEscapeHTML: true, | |||
| } | |||
| return r | |||
| } | |||
| // NewRequest is an alias for method `R()`. Creates a new request instance, its used for | |||
| // Get, Post, Put, Delete, Patch, Head, Options, etc. | |||
| func (c *Client) NewRequest() *Request { | |||
| return c.R() | |||
| } | |||
| // OnBeforeRequest method appends request middleware into the before request chain. | |||
| // Its gets applied after default Resty request middlewares and before request | |||
| // been sent from Resty to host server. | |||
| // client.OnBeforeRequest(func(c *resty.Client, r *resty.Request) error { | |||
| // // Now you have access to Client and Request instance | |||
| // // manipulate it as per your need | |||
| // | |||
| // return nil // if its success otherwise return error | |||
| // }) | |||
| func (c *Client) OnBeforeRequest(m RequestMiddleware) *Client { | |||
| c.udBeforeRequest = append(c.udBeforeRequest, m) | |||
| return c | |||
| } | |||
| // OnAfterResponse method appends response middleware into the after response chain. | |||
| // Once we receive response from host server, default Resty response middleware | |||
| // gets applied and then user assigened response middlewares applied. | |||
| // client.OnAfterResponse(func(c *resty.Client, r *resty.Response) error { | |||
| // // Now you have access to Client and Response instance | |||
| // // manipulate it as per your need | |||
| // | |||
| // return nil // if its success otherwise return error | |||
| // }) | |||
| func (c *Client) OnAfterResponse(m ResponseMiddleware) *Client { | |||
| c.afterResponse = append(c.afterResponse, m) | |||
| return c | |||
| } | |||
| // SetPreRequestHook method sets the given pre-request function into resty client. | |||
| // It is called right before the request is fired. | |||
| // | |||
| // Note: Only one pre-request hook can be registered. Use `client.OnBeforeRequest` for mutilple. | |||
| func (c *Client) SetPreRequestHook(h PreRequestHook) *Client { | |||
| if c.preReqHook != nil { | |||
| c.log.Warnf("Overwriting an existing pre-request hook: %s", functionName(h)) | |||
| } | |||
| c.preReqHook = h | |||
| return c | |||
| } | |||
| // SetDebug method enables the debug mode on Resty client. Client logs details of every request and response. | |||
| // For `Request` it logs information such as HTTP verb, Relative URL path, Host, Headers, Body if it has one. | |||
| // For `Response` it logs information such as Status, Response Time, Headers, Body if it has one. | |||
| // client.SetDebug(true) | |||
| func (c *Client) SetDebug(d bool) *Client { | |||
| c.Debug = d | |||
| return c | |||
| } | |||
| // SetDebugBodyLimit sets the maximum size for which the response and request body will be logged in debug mode. | |||
| // client.SetDebugBodyLimit(1000000) | |||
| func (c *Client) SetDebugBodyLimit(sl int64) *Client { | |||
| c.debugBodySizeLimit = sl | |||
| return c | |||
| } | |||
| // OnRequestLog method used to set request log callback into Resty. Registered callback gets | |||
| // called before the resty actually logs the information. | |||
| func (c *Client) OnRequestLog(rl RequestLogCallback) *Client { | |||
| if c.requestLog != nil { | |||
| c.log.Warnf("Overwriting an existing on-request-log callback from=%s to=%s", | |||
| functionName(c.requestLog), functionName(rl)) | |||
| } | |||
| c.requestLog = rl | |||
| return c | |||
| } | |||
| // OnResponseLog method used to set response log callback into Resty. Registered callback gets | |||
| // called before the resty actually logs the information. | |||
| func (c *Client) OnResponseLog(rl ResponseLogCallback) *Client { | |||
| if c.responseLog != nil { | |||
| c.log.Warnf("Overwriting an existing on-response-log callback from=%s to=%s", | |||
| functionName(c.responseLog), functionName(rl)) | |||
| } | |||
| c.responseLog = rl | |||
| return c | |||
| } | |||
| // SetDisableWarn method disables the warning message on Resty client. | |||
| // | |||
| // For Example: Resty warns the user when BasicAuth used on non-TLS mode. | |||
| // client.SetDisableWarn(true) | |||
| func (c *Client) SetDisableWarn(d bool) *Client { | |||
| c.DisableWarn = d | |||
| return c | |||
| } | |||
| // SetAllowGetMethodPayload method allows the GET method with payload on Resty client. | |||
| // | |||
| // For Example: Resty allows the user sends request with a payload on HTTP GET method. | |||
| // client.SetAllowGetMethodPayload(true) | |||
| func (c *Client) SetAllowGetMethodPayload(a bool) *Client { | |||
| c.AllowGetMethodPayload = a | |||
| return c | |||
| } | |||
| // SetLogger method sets given writer for logging Resty request and response details. | |||
| // | |||
| // Compliant to interface `resty.Logger`. | |||
| func (c *Client) SetLogger(l Logger) *Client { | |||
| c.log = l | |||
| return c | |||
| } | |||
| // SetContentLength method enables the HTTP header `Content-Length` value for every request. | |||
| // By default Resty won't set `Content-Length`. | |||
| // client.SetContentLength(true) | |||
| // | |||
| // Also you have an option to enable for particular request. See `Request.SetContentLength` | |||
| func (c *Client) SetContentLength(l bool) *Client { | |||
| c.setContentLength = l | |||
| return c | |||
| } | |||
| // SetTimeout method sets timeout for request raised from client. | |||
| // client.SetTimeout(time.Duration(1 * time.Minute)) | |||
| func (c *Client) SetTimeout(timeout time.Duration) *Client { | |||
| c.httpClient.Timeout = timeout | |||
| return c | |||
| } | |||
| // SetError method is to register the global or client common `Error` object into Resty. | |||
| // It is used for automatic unmarshalling if response status code is greater than 399 and | |||
| // content type either JSON or XML. Can be pointer or non-pointer. | |||
| // client.SetError(&Error{}) | |||
| // // OR | |||
| // client.SetError(Error{}) | |||
| func (c *Client) SetError(err interface{}) *Client { | |||
| c.Error = typeOf(err) | |||
| return c | |||
| } | |||
| // SetRedirectPolicy method sets the client redirect poilicy. Resty provides ready to use | |||
| // redirect policies. Wanna create one for yourself refer to `redirect.go`. | |||
| // | |||
| // client.SetRedirectPolicy(FlexibleRedirectPolicy(20)) | |||
| // | |||
| // // Need multiple redirect policies together | |||
| // client.SetRedirectPolicy(FlexibleRedirectPolicy(20), DomainCheckRedirectPolicy("host1.com", "host2.net")) | |||
| func (c *Client) SetRedirectPolicy(policies ...interface{}) *Client { | |||
| for _, p := range policies { | |||
| if _, ok := p.(RedirectPolicy); !ok { | |||
| c.log.Errorf("%v does not implement resty.RedirectPolicy (missing Apply method)", | |||
| functionName(p)) | |||
| } | |||
| } | |||
| c.httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { | |||
| for _, p := range policies { | |||
| if err := p.(RedirectPolicy).Apply(req, via); err != nil { | |||
| return err | |||
| } | |||
| } | |||
| return nil // looks good, go ahead | |||
| } | |||
| return c | |||
| } | |||
| // SetRetryCount method enables retry on Resty client and allows you | |||
| // to set no. of retry count. Resty uses a Backoff mechanism. | |||
| func (c *Client) SetRetryCount(count int) *Client { | |||
| c.RetryCount = count | |||
| return c | |||
| } | |||
| // SetRetryWaitTime method sets default wait time to sleep before retrying | |||
| // request. | |||
| // | |||
| // Default is 100 milliseconds. | |||
| func (c *Client) SetRetryWaitTime(waitTime time.Duration) *Client { | |||
| c.RetryWaitTime = waitTime | |||
| return c | |||
| } | |||
| // SetRetryMaxWaitTime method sets max wait time to sleep before retrying | |||
| // request. | |||
| // | |||
| // Default is 2 seconds. | |||
| func (c *Client) SetRetryMaxWaitTime(maxWaitTime time.Duration) *Client { | |||
| c.RetryMaxWaitTime = maxWaitTime | |||
| return c | |||
| } | |||
| // SetRetryAfter sets callback to calculate wait time between retries. | |||
| // Default (nil) implies exponential backoff with jitter | |||
| func (c *Client) SetRetryAfter(callback RetryAfterFunc) *Client { | |||
| c.RetryAfter = callback | |||
| return c | |||
| } | |||
| // AddRetryCondition method adds a retry condition function to array of functions | |||
| // that are checked to determine if the request is retried. The request will | |||
| // retry if any of the functions return true and error is nil. | |||
| func (c *Client) AddRetryCondition(condition RetryConditionFunc) *Client { | |||
| c.RetryConditions = append(c.RetryConditions, condition) | |||
| return c | |||
| } | |||
| // SetTLSClientConfig method sets TLSClientConfig for underling client Transport. | |||
| // | |||
| // For Example: | |||
| // // One can set custom root-certificate. Refer: http://golang.org/pkg/crypto/tls/#example_Dial | |||
| // client.SetTLSClientConfig(&tls.Config{ RootCAs: roots }) | |||
| // | |||
| // // or One can disable security check (https) | |||
| // client.SetTLSClientConfig(&tls.Config{ InsecureSkipVerify: true }) | |||
| // | |||
| // Note: This method overwrites existing `TLSClientConfig`. | |||
| func (c *Client) SetTLSClientConfig(config *tls.Config) *Client { | |||
| transport, err := c.transport() | |||
| if err != nil { | |||
| c.log.Errorf("%v", err) | |||
| return c | |||
| } | |||
| transport.TLSClientConfig = config | |||
| return c | |||
| } | |||
| // SetProxy method sets the Proxy URL and Port for Resty client. | |||
| // client.SetProxy("http://proxyserver:8888") | |||
| // | |||
| // OR Without this `SetProxy` method, you could also set Proxy via environment variable. | |||
| // | |||
| // Refer to godoc `http.ProxyFromEnvironment`. | |||
| func (c *Client) SetProxy(proxyURL string) *Client { | |||
| transport, err := c.transport() | |||
| if err != nil { | |||
| c.log.Errorf("%v", err) | |||
| return c | |||
| } | |||
| pURL, err := url.Parse(proxyURL) | |||
| if err != nil { | |||
| c.log.Errorf("%v", err) | |||
| return c | |||
| } | |||
| c.proxyURL = pURL | |||
| transport.Proxy = http.ProxyURL(c.proxyURL) | |||
| return c | |||
| } | |||
| // RemoveProxy method removes the proxy configuration from Resty client | |||
| // client.RemoveProxy() | |||
| func (c *Client) RemoveProxy() *Client { | |||
| transport, err := c.transport() | |||
| if err != nil { | |||
| c.log.Errorf("%v", err) | |||
| return c | |||
| } | |||
| c.proxyURL = nil | |||
| transport.Proxy = nil | |||
| return c | |||
| } | |||
| // SetCertificates method helps to set client certificates into Resty conveniently. | |||
| func (c *Client) SetCertificates(certs ...tls.Certificate) *Client { | |||
| config, err := c.tlsConfig() | |||
| if err != nil { | |||
| c.log.Errorf("%v", err) | |||
| return c | |||
| } | |||
| config.Certificates = append(config.Certificates, certs...) | |||
| return c | |||
| } | |||
| // SetRootCertificate method helps to add one or more root certificates into Resty client | |||
| // client.SetRootCertificate("/path/to/root/pemFile.pem") | |||
| func (c *Client) SetRootCertificate(pemFilePath string) *Client { | |||
| rootPemData, err := ioutil.ReadFile(pemFilePath) | |||
| if err != nil { | |||
| c.log.Errorf("%v", err) | |||
| return c | |||
| } | |||
| config, err := c.tlsConfig() | |||
| if err != nil { | |||
| c.log.Errorf("%v", err) | |||
| return c | |||
| } | |||
| if config.RootCAs == nil { | |||
| config.RootCAs = x509.NewCertPool() | |||
| } | |||
| config.RootCAs.AppendCertsFromPEM(rootPemData) | |||
| return c | |||
| } | |||
| // SetRootCertificateFromString method helps to add one or more root certificates into Resty client | |||
| // client.SetRootCertificateFromString("pem file content") | |||
| func (c *Client) SetRootCertificateFromString(pemContent string) *Client { | |||
| config, err := c.tlsConfig() | |||
| if err != nil { | |||
| c.log.Errorf("%v", err) | |||
| return c | |||
| } | |||
| if config.RootCAs == nil { | |||
| config.RootCAs = x509.NewCertPool() | |||
| } | |||
| config.RootCAs.AppendCertsFromPEM([]byte(pemContent)) | |||
| return c | |||
| } | |||
| // SetOutputDirectory method sets output directory for saving HTTP response into file. | |||
| // If the output directory not exists then resty creates one. This setting is optional one, | |||
| // if you're planning using absolute path in `Request.SetOutput` and can used together. | |||
| // client.SetOutputDirectory("/save/http/response/here") | |||
| func (c *Client) SetOutputDirectory(dirPath string) *Client { | |||
| c.outputDirectory = dirPath | |||
| return c | |||
| } | |||
| // SetTransport method sets custom `*http.Transport` or any `http.RoundTripper` | |||
| // compatible interface implementation in the resty client. | |||
| // | |||
| // Note: | |||
| // | |||
| // - If transport is not type of `*http.Transport` then you may not be able to | |||
| // take advantage of some of the Resty client settings. | |||
| // | |||
| // - It overwrites the Resty client transport instance and it's configurations. | |||
| // | |||
| // transport := &http.Transport{ | |||
| // // somthing like Proxying to httptest.Server, etc... | |||
| // Proxy: func(req *http.Request) (*url.URL, error) { | |||
| // return url.Parse(server.URL) | |||
| // }, | |||
| // } | |||
| // | |||
| // client.SetTransport(transport) | |||
| func (c *Client) SetTransport(transport http.RoundTripper) *Client { | |||
| if transport != nil { | |||
| c.httpClient.Transport = transport | |||
| } | |||
| return c | |||
| } | |||
| // SetScheme method sets custom scheme in the Resty client. It's way to override default. | |||
| // client.SetScheme("http") | |||
| func (c *Client) SetScheme(scheme string) *Client { | |||
| if !IsStringEmpty(scheme) { | |||
| c.scheme = scheme | |||
| } | |||
| return c | |||
| } | |||
| // SetCloseConnection method sets variable `Close` in http request struct with the given | |||
| // value. More info: https://golang.org/src/net/http/request.go | |||
| func (c *Client) SetCloseConnection(close bool) *Client { | |||
| c.closeConnection = close | |||
| return c | |||
| } | |||
| // SetDoNotParseResponse method instructs `Resty` not to parse the response body automatically. | |||
| // Resty exposes the raw response body as `io.ReadCloser`. Also do not forget to close the body, | |||
| // otherwise you might get into connection leaks, no connection reuse. | |||
| // | |||
| // Note: Response middlewares are not applicable, if you use this option. Basically you have | |||
| // taken over the control of response parsing from `Resty`. | |||
| func (c *Client) SetDoNotParseResponse(parse bool) *Client { | |||
| c.notParseResponse = parse | |||
| return c | |||
| } | |||
| // SetPathParams method sets multiple URL path key-value pairs at one go in the | |||
| // Resty client instance. | |||
| // client.SetPathParams(map[string]string{ | |||
| // "userId": "sample@sample.com", | |||
| // "subAccountId": "100002", | |||
| // }) | |||
| // | |||
| // Result: | |||
| // URL - /v1/users/{userId}/{subAccountId}/details | |||
| // Composed URL - /v1/users/sample@sample.com/100002/details | |||
| // It replace the value of the key while composing request URL. Also it can be | |||
| // overridden at request level Path Params options, see `Request.SetPathParams`. | |||
| func (c *Client) SetPathParams(params map[string]string) *Client { | |||
| for p, v := range params { | |||
| c.pathParams[p] = v | |||
| } | |||
| return c | |||
| } | |||
| // SetJSONEscapeHTML method is to enable/disable the HTML escape on JSON marshal. | |||
| // | |||
| // Note: This option only applicable to standard JSON Marshaller. | |||
| func (c *Client) SetJSONEscapeHTML(b bool) *Client { | |||
| c.jsonEscapeHTML = b | |||
| return c | |||
| } | |||
| // EnableTrace method enables the Resty client trace for the requests fired from | |||
| // the client using `httptrace.ClientTrace` and provides insights. | |||
| // | |||
| // client := resty.New().EnableTrace() | |||
| // | |||
| // resp, err := client.R().Get("https://httpbin.org/get") | |||
| // fmt.Println("Error:", err) | |||
| // fmt.Println("Trace Info:", resp.Request.TraceInfo()) | |||
| // | |||
| // Also `Request.EnableTrace` available too to get trace info for single request. | |||
| // | |||
| // Since v2.0.0 | |||
| func (c *Client) EnableTrace() *Client { | |||
| c.trace = true | |||
| return c | |||
| } | |||
| // DisableTrace method disables the Resty client trace. Refer to `Client.EnableTrace`. | |||
| // | |||
| // Since v2.0.0 | |||
| func (c *Client) DisableTrace() *Client { | |||
| c.trace = false | |||
| return c | |||
| } | |||
| // IsProxySet method returns the true is proxy is set from resty client otherwise | |||
| // false. By default proxy is set from environment, refer to `http.ProxyFromEnvironment`. | |||
| func (c *Client) IsProxySet() bool { | |||
| return c.proxyURL != nil | |||
| } | |||
| // GetClient method returns the current `http.Client` used by the resty client. | |||
| func (c *Client) GetClient() *http.Client { | |||
| return c.httpClient | |||
| } | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // Client Unexported methods | |||
| //_______________________________________________________________________ | |||
| // Executes method executes the given `Request` object and returns response | |||
| // error. | |||
| func (c *Client) execute(req *Request) (*Response, error) { | |||
| defer releaseBuffer(req.bodyBuf) | |||
| // Apply Request middleware | |||
| var err error | |||
| // user defined on before request methods | |||
| // to modify the *resty.Request object | |||
| for _, f := range c.udBeforeRequest { | |||
| if err = f(c, req); err != nil { | |||
| return nil, wrapNoRetryErr(err) | |||
| } | |||
| } | |||
| // resty middlewares | |||
| for _, f := range c.beforeRequest { | |||
| if err = f(c, req); err != nil { | |||
| return nil, wrapNoRetryErr(err) | |||
| } | |||
| } | |||
| if hostHeader := req.Header.Get("Host"); hostHeader != "" { | |||
| req.RawRequest.Host = hostHeader | |||
| } | |||
| // call pre-request if defined | |||
| if c.preReqHook != nil { | |||
| if err = c.preReqHook(c, req.RawRequest); err != nil { | |||
| return nil, wrapNoRetryErr(err) | |||
| } | |||
| } | |||
| if err = requestLogger(c, req); err != nil { | |||
| return nil, wrapNoRetryErr(err) | |||
| } | |||
| req.Time = time.Now() | |||
| resp, err := c.httpClient.Do(req.RawRequest) | |||
| response := &Response{ | |||
| Request: req, | |||
| RawResponse: resp, | |||
| } | |||
| if err != nil || req.notParseResponse || c.notParseResponse { | |||
| response.setReceivedAt() | |||
| return response, err | |||
| } | |||
| if !req.isSaveResponse { | |||
| defer closeq(resp.Body) | |||
| body := resp.Body | |||
| // GitHub #142 & #187 | |||
| if strings.EqualFold(resp.Header.Get(hdrContentEncodingKey), "gzip") && resp.ContentLength != 0 { | |||
| if _, ok := body.(*gzip.Reader); !ok { | |||
| body, err = gzip.NewReader(body) | |||
| if err != nil { | |||
| response.setReceivedAt() | |||
| return response, err | |||
| } | |||
| defer closeq(body) | |||
| } | |||
| } | |||
| if response.body, err = ioutil.ReadAll(body); err != nil { | |||
| response.setReceivedAt() | |||
| return response, err | |||
| } | |||
| response.setReceivedAt() // after we read the body | |||
| response.size = int64(len(response.body)) | |||
| } | |||
| // Apply Response middleware | |||
| for _, f := range c.afterResponse { | |||
| if err = f(c, response); err != nil { | |||
| break | |||
| } | |||
| } | |||
| return response, wrapNoRetryErr(err) | |||
| } | |||
| // getting TLS client config if not exists then create one | |||
| func (c *Client) tlsConfig() (*tls.Config, error) { | |||
| transport, err := c.transport() | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| if transport.TLSClientConfig == nil { | |||
| transport.TLSClientConfig = &tls.Config{} | |||
| } | |||
| return transport.TLSClientConfig, nil | |||
| } | |||
| // Transport method returns `*http.Transport` currently in use or error | |||
| // in case currently used `transport` is not a `*http.Transport`. | |||
| func (c *Client) transport() (*http.Transport, error) { | |||
| if transport, ok := c.httpClient.Transport.(*http.Transport); ok { | |||
| return transport, nil | |||
| } | |||
| return nil, errors.New("current transport is not an *http.Transport instance") | |||
| } | |||
| // just an internal helper method | |||
| func (c *Client) outputLogTo(w io.Writer) *Client { | |||
| c.log.(*logger).l.SetOutput(w) | |||
| return c | |||
| } | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // File struct and its methods | |||
| //_______________________________________________________________________ | |||
| // File struct represent file information for multipart request | |||
| type File struct { | |||
| Name string | |||
| ParamName string | |||
| io.Reader | |||
| } | |||
| // String returns string value of current file details | |||
| func (f *File) String() string { | |||
| return fmt.Sprintf("ParamName: %v; FileName: %v", f.ParamName, f.Name) | |||
| } | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // MultipartField struct | |||
| //_______________________________________________________________________ | |||
| // MultipartField struct represent custom data part for multipart request | |||
| type MultipartField struct { | |||
| Param string | |||
| FileName string | |||
| ContentType string | |||
| io.Reader | |||
| } | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // Unexported package methods | |||
| //_______________________________________________________________________ | |||
| func createClient(hc *http.Client) *Client { | |||
| if hc.Transport == nil { | |||
| hc.Transport = createTransport(nil) | |||
| } | |||
| c := &Client{ // not setting lang default values | |||
| QueryParam: url.Values{}, | |||
| FormData: url.Values{}, | |||
| Header: http.Header{}, | |||
| Cookies: make([]*http.Cookie, 0), | |||
| RetryWaitTime: defaultWaitTime, | |||
| RetryMaxWaitTime: defaultMaxWaitTime, | |||
| JSONMarshal: json.Marshal, | |||
| JSONUnmarshal: json.Unmarshal, | |||
| jsonEscapeHTML: true, | |||
| httpClient: hc, | |||
| debugBodySizeLimit: math.MaxInt32, | |||
| pathParams: make(map[string]string), | |||
| } | |||
| // Logger | |||
| c.SetLogger(createLogger()) | |||
| // default before request middlewares | |||
| c.beforeRequest = []RequestMiddleware{ | |||
| parseRequestURL, | |||
| parseRequestHeader, | |||
| parseRequestBody, | |||
| createHTTPRequest, | |||
| addCredentials, | |||
| } | |||
| // user defined request middlewares | |||
| c.udBeforeRequest = []RequestMiddleware{} | |||
| // default after response middlewares | |||
| c.afterResponse = []ResponseMiddleware{ | |||
| responseLogger, | |||
| parseResponseBody, | |||
| saveResponseIntoFile, | |||
| } | |||
| return c | |||
| } | |||
| @@ -0,0 +1,5 @@ | |||
| module github.com/go-resty/resty/v2 | |||
| require golang.org/x/net v0.0.0-20200513185701-a91f0712d120 | |||
| go 1.11 | |||
| @@ -0,0 +1,526 @@ | |||
| // Copyright (c) 2015-2020 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. | |||
| // resty source code and usage is governed by a MIT style | |||
| // license that can be found in the LICENSE file. | |||
| package resty | |||
| import ( | |||
| "bytes" | |||
| "encoding/xml" | |||
| "errors" | |||
| "fmt" | |||
| "io" | |||
| "io/ioutil" | |||
| "mime/multipart" | |||
| "net/http" | |||
| "net/url" | |||
| "os" | |||
| "path/filepath" | |||
| "reflect" | |||
| "strings" | |||
| "time" | |||
| ) | |||
| const debugRequestLogKey = "__restyDebugRequestLog" | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // Request Middleware(s) | |||
| //_______________________________________________________________________ | |||
| func parseRequestURL(c *Client, r *Request) error { | |||
| // GitHub #103 Path Params | |||
| if len(r.pathParams) > 0 { | |||
| for p, v := range r.pathParams { | |||
| r.URL = strings.Replace(r.URL, "{"+p+"}", url.PathEscape(v), -1) | |||
| } | |||
| } | |||
| if len(c.pathParams) > 0 { | |||
| for p, v := range c.pathParams { | |||
| r.URL = strings.Replace(r.URL, "{"+p+"}", url.PathEscape(v), -1) | |||
| } | |||
| } | |||
| // Parsing request URL | |||
| reqURL, err := url.Parse(r.URL) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| // If Request.URL is relative path then added c.HostURL into | |||
| // the request URL otherwise Request.URL will be used as-is | |||
| if !reqURL.IsAbs() { | |||
| r.URL = reqURL.String() | |||
| if len(r.URL) > 0 && r.URL[0] != '/' { | |||
| r.URL = "/" + r.URL | |||
| } | |||
| reqURL, err = url.Parse(c.HostURL + r.URL) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| } | |||
| // Adding Query Param | |||
| query := make(url.Values) | |||
| for k, v := range c.QueryParam { | |||
| for _, iv := range v { | |||
| query.Add(k, iv) | |||
| } | |||
| } | |||
| for k, v := range r.QueryParam { | |||
| // remove query param from client level by key | |||
| // since overrides happens for that key in the request | |||
| query.Del(k) | |||
| for _, iv := range v { | |||
| query.Add(k, iv) | |||
| } | |||
| } | |||
| // GitHub #123 Preserve query string order partially. | |||
| // Since not feasible in `SetQuery*` resty methods, because | |||
| // standard package `url.Encode(...)` sorts the query params | |||
| // alphabetically | |||
| if len(query) > 0 { | |||
| if IsStringEmpty(reqURL.RawQuery) { | |||
| reqURL.RawQuery = query.Encode() | |||
| } else { | |||
| reqURL.RawQuery = reqURL.RawQuery + "&" + query.Encode() | |||
| } | |||
| } | |||
| r.URL = reqURL.String() | |||
| return nil | |||
| } | |||
| func parseRequestHeader(c *Client, r *Request) error { | |||
| hdr := make(http.Header) | |||
| for k := range c.Header { | |||
| hdr[k] = append(hdr[k], c.Header[k]...) | |||
| } | |||
| for k := range r.Header { | |||
| hdr.Del(k) | |||
| hdr[k] = append(hdr[k], r.Header[k]...) | |||
| } | |||
| if IsStringEmpty(hdr.Get(hdrUserAgentKey)) { | |||
| hdr.Set(hdrUserAgentKey, hdrUserAgentValue) | |||
| } | |||
| ct := hdr.Get(hdrContentTypeKey) | |||
| if IsStringEmpty(hdr.Get(hdrAcceptKey)) && !IsStringEmpty(ct) && | |||
| (IsJSONType(ct) || IsXMLType(ct)) { | |||
| hdr.Set(hdrAcceptKey, hdr.Get(hdrContentTypeKey)) | |||
| } | |||
| r.Header = hdr | |||
| return nil | |||
| } | |||
| func parseRequestBody(c *Client, r *Request) (err error) { | |||
| if isPayloadSupported(r.Method, c.AllowGetMethodPayload) { | |||
| // Handling Multipart | |||
| if r.isMultiPart && !(r.Method == MethodPatch) { | |||
| if err = handleMultipart(c, r); err != nil { | |||
| return | |||
| } | |||
| goto CL | |||
| } | |||
| // Handling Form Data | |||
| if len(c.FormData) > 0 || len(r.FormData) > 0 { | |||
| handleFormData(c, r) | |||
| goto CL | |||
| } | |||
| // Handling Request body | |||
| if r.Body != nil { | |||
| handleContentType(c, r) | |||
| if err = handleRequestBody(c, r); err != nil { | |||
| return | |||
| } | |||
| } | |||
| } | |||
| CL: | |||
| // by default resty won't set content length, you can if you want to :) | |||
| if (c.setContentLength || r.setContentLength) && r.bodyBuf != nil { | |||
| r.Header.Set(hdrContentLengthKey, fmt.Sprintf("%d", r.bodyBuf.Len())) | |||
| } | |||
| return | |||
| } | |||
| func createHTTPRequest(c *Client, r *Request) (err error) { | |||
| if r.bodyBuf == nil { | |||
| if reader, ok := r.Body.(io.Reader); ok { | |||
| r.RawRequest, err = http.NewRequest(r.Method, r.URL, reader) | |||
| } else { | |||
| r.RawRequest, err = http.NewRequest(r.Method, r.URL, nil) | |||
| } | |||
| } else { | |||
| r.RawRequest, err = http.NewRequest(r.Method, r.URL, r.bodyBuf) | |||
| } | |||
| if err != nil { | |||
| return | |||
| } | |||
| // Assign close connection option | |||
| r.RawRequest.Close = c.closeConnection | |||
| // Add headers into http request | |||
| r.RawRequest.Header = r.Header | |||
| // Add cookies from client instance into http request | |||
| for _, cookie := range c.Cookies { | |||
| r.RawRequest.AddCookie(cookie) | |||
| } | |||
| // Add cookies from request instance into http request | |||
| for _, cookie := range r.Cookies { | |||
| r.RawRequest.AddCookie(cookie) | |||
| } | |||
| // it's for non-http scheme option | |||
| if r.RawRequest.URL != nil && r.RawRequest.URL.Scheme == "" { | |||
| r.RawRequest.URL.Scheme = c.scheme | |||
| r.RawRequest.URL.Host = r.URL | |||
| } | |||
| // Enable trace | |||
| if c.trace || r.trace { | |||
| r.clientTrace = &clientTrace{} | |||
| r.ctx = r.clientTrace.createContext(r.Context()) | |||
| } | |||
| // Use context if it was specified | |||
| if r.ctx != nil { | |||
| r.RawRequest = r.RawRequest.WithContext(r.ctx) | |||
| } | |||
| // assign get body func for the underlying raw request instance | |||
| r.RawRequest.GetBody = func() (io.ReadCloser, error) { | |||
| // If r.bodyBuf present, return the copy | |||
| if r.bodyBuf != nil { | |||
| return ioutil.NopCloser(bytes.NewReader(r.bodyBuf.Bytes())), nil | |||
| } | |||
| // Maybe body is `io.Reader`. | |||
| // Note: Resty user have to watchout for large body size of `io.Reader` | |||
| if r.RawRequest.Body != nil { | |||
| b, err := ioutil.ReadAll(r.RawRequest.Body) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| // Restore the Body | |||
| closeq(r.RawRequest.Body) | |||
| r.RawRequest.Body = ioutil.NopCloser(bytes.NewBuffer(b)) | |||
| // Return the Body bytes | |||
| return ioutil.NopCloser(bytes.NewBuffer(b)), nil | |||
| } | |||
| return nil, nil | |||
| } | |||
| return | |||
| } | |||
| func addCredentials(c *Client, r *Request) error { | |||
| var isBasicAuth bool | |||
| // Basic Auth | |||
| if r.UserInfo != nil { // takes precedence | |||
| r.RawRequest.SetBasicAuth(r.UserInfo.Username, r.UserInfo.Password) | |||
| isBasicAuth = true | |||
| } else if c.UserInfo != nil { | |||
| r.RawRequest.SetBasicAuth(c.UserInfo.Username, c.UserInfo.Password) | |||
| isBasicAuth = true | |||
| } | |||
| if !c.DisableWarn { | |||
| if isBasicAuth && !strings.HasPrefix(r.URL, "https") { | |||
| c.log.Warnf("Using Basic Auth in HTTP mode is not secure, use HTTPS") | |||
| } | |||
| } | |||
| // Set the Authorization Header Scheme | |||
| var authScheme string | |||
| if !IsStringEmpty(r.AuthScheme) { | |||
| authScheme = r.AuthScheme | |||
| } else if !IsStringEmpty(c.AuthScheme) { | |||
| authScheme = c.AuthScheme | |||
| } else { | |||
| authScheme = "Bearer" | |||
| } | |||
| // Build the Token Auth header | |||
| if !IsStringEmpty(r.Token) { // takes precedence | |||
| r.RawRequest.Header.Set(hdrAuthorizationKey, authScheme+" "+r.Token) | |||
| } else if !IsStringEmpty(c.Token) { | |||
| r.RawRequest.Header.Set(hdrAuthorizationKey, authScheme+" "+c.Token) | |||
| } | |||
| return nil | |||
| } | |||
| func requestLogger(c *Client, r *Request) error { | |||
| if c.Debug { | |||
| rr := r.RawRequest | |||
| rl := &RequestLog{Header: copyHeaders(rr.Header), Body: r.fmtBodyString(c.debugBodySizeLimit)} | |||
| if c.requestLog != nil { | |||
| if err := c.requestLog(rl); err != nil { | |||
| return err | |||
| } | |||
| } | |||
| // fmt.Sprintf("COOKIES:\n%s\n", composeCookies(c.GetClient().Jar, *rr.URL)) + | |||
| reqLog := "\n==============================================================================\n" + | |||
| "~~~ REQUEST ~~~\n" + | |||
| fmt.Sprintf("%s %s %s\n", r.Method, rr.URL.RequestURI(), rr.Proto) + | |||
| fmt.Sprintf("HOST : %s\n", rr.URL.Host) + | |||
| fmt.Sprintf("HEADERS:\n%s\n", composeHeaders(c, r, rl.Header)) + | |||
| fmt.Sprintf("BODY :\n%v\n", rl.Body) + | |||
| "------------------------------------------------------------------------------\n" | |||
| r.initValuesMap() | |||
| r.values[debugRequestLogKey] = reqLog | |||
| } | |||
| return nil | |||
| } | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // Response Middleware(s) | |||
| //_______________________________________________________________________ | |||
| func responseLogger(c *Client, res *Response) error { | |||
| if c.Debug { | |||
| rl := &ResponseLog{Header: copyHeaders(res.Header()), Body: res.fmtBodyString(c.debugBodySizeLimit)} | |||
| if c.responseLog != nil { | |||
| if err := c.responseLog(rl); err != nil { | |||
| return err | |||
| } | |||
| } | |||
| debugLog := res.Request.values[debugRequestLogKey].(string) | |||
| debugLog += "~~~ RESPONSE ~~~\n" + | |||
| fmt.Sprintf("STATUS : %s\n", res.Status()) + | |||
| fmt.Sprintf("PROTO : %s\n", res.RawResponse.Proto) + | |||
| fmt.Sprintf("RECEIVED AT : %v\n", res.ReceivedAt().Format(time.RFC3339Nano)) + | |||
| fmt.Sprintf("TIME DURATION: %v\n", res.Time()) + | |||
| "HEADERS :\n" + | |||
| composeHeaders(c, res.Request, rl.Header) + "\n" | |||
| if res.Request.isSaveResponse { | |||
| debugLog += fmt.Sprintf("BODY :\n***** RESPONSE WRITTEN INTO FILE *****\n") | |||
| } else { | |||
| debugLog += fmt.Sprintf("BODY :\n%v\n", rl.Body) | |||
| } | |||
| debugLog += "==============================================================================\n" | |||
| c.log.Debugf("%s", debugLog) | |||
| } | |||
| return nil | |||
| } | |||
| func parseResponseBody(c *Client, res *Response) (err error) { | |||
| if res.StatusCode() == http.StatusNoContent { | |||
| return | |||
| } | |||
| // Handles only JSON or XML content type | |||
| ct := firstNonEmpty(res.Request.forceContentType, res.Header().Get(hdrContentTypeKey), res.Request.fallbackContentType) | |||
| if IsJSONType(ct) || IsXMLType(ct) { | |||
| // HTTP status code > 199 and < 300, considered as Result | |||
| if res.IsSuccess() { | |||
| res.Request.Error = nil | |||
| if res.Request.Result != nil { | |||
| err = Unmarshalc(c, ct, res.body, res.Request.Result) | |||
| return | |||
| } | |||
| } | |||
| // HTTP status code > 399, considered as Error | |||
| if res.IsError() { | |||
| // global error interface | |||
| if res.Request.Error == nil && c.Error != nil { | |||
| res.Request.Error = reflect.New(c.Error).Interface() | |||
| } | |||
| if res.Request.Error != nil { | |||
| err = Unmarshalc(c, ct, res.body, res.Request.Error) | |||
| } | |||
| } | |||
| } | |||
| return | |||
| } | |||
| func handleMultipart(c *Client, r *Request) (err error) { | |||
| r.bodyBuf = acquireBuffer() | |||
| w := multipart.NewWriter(r.bodyBuf) | |||
| for k, v := range c.FormData { | |||
| for _, iv := range v { | |||
| if err = w.WriteField(k, iv); err != nil { | |||
| return err | |||
| } | |||
| } | |||
| } | |||
| for k, v := range r.FormData { | |||
| for _, iv := range v { | |||
| if strings.HasPrefix(k, "@") { // file | |||
| err = addFile(w, k[1:], iv) | |||
| if err != nil { | |||
| return | |||
| } | |||
| } else { // form value | |||
| if err = w.WriteField(k, iv); err != nil { | |||
| return err | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // #21 - adding io.Reader support | |||
| if len(r.multipartFiles) > 0 { | |||
| for _, f := range r.multipartFiles { | |||
| err = addFileReader(w, f) | |||
| if err != nil { | |||
| return | |||
| } | |||
| } | |||
| } | |||
| // GitHub #130 adding multipart field support with content type | |||
| if len(r.multipartFields) > 0 { | |||
| for _, mf := range r.multipartFields { | |||
| if err = addMultipartFormField(w, mf); err != nil { | |||
| return | |||
| } | |||
| } | |||
| } | |||
| r.Header.Set(hdrContentTypeKey, w.FormDataContentType()) | |||
| err = w.Close() | |||
| return | |||
| } | |||
| func handleFormData(c *Client, r *Request) { | |||
| formData := url.Values{} | |||
| for k, v := range c.FormData { | |||
| for _, iv := range v { | |||
| formData.Add(k, iv) | |||
| } | |||
| } | |||
| for k, v := range r.FormData { | |||
| // remove form data field from client level by key | |||
| // since overrides happens for that key in the request | |||
| formData.Del(k) | |||
| for _, iv := range v { | |||
| formData.Add(k, iv) | |||
| } | |||
| } | |||
| r.bodyBuf = bytes.NewBuffer([]byte(formData.Encode())) | |||
| r.Header.Set(hdrContentTypeKey, formContentType) | |||
| r.isFormData = true | |||
| } | |||
| func handleContentType(c *Client, r *Request) { | |||
| contentType := r.Header.Get(hdrContentTypeKey) | |||
| if IsStringEmpty(contentType) { | |||
| contentType = DetectContentType(r.Body) | |||
| r.Header.Set(hdrContentTypeKey, contentType) | |||
| } | |||
| } | |||
| func handleRequestBody(c *Client, r *Request) (err error) { | |||
| var bodyBytes []byte | |||
| contentType := r.Header.Get(hdrContentTypeKey) | |||
| kind := kindOf(r.Body) | |||
| r.bodyBuf = nil | |||
| if reader, ok := r.Body.(io.Reader); ok { | |||
| if c.setContentLength || r.setContentLength { // keep backward compatibility | |||
| r.bodyBuf = acquireBuffer() | |||
| _, err = r.bodyBuf.ReadFrom(reader) | |||
| r.Body = nil | |||
| } else { | |||
| // Otherwise buffer less processing for `io.Reader`, sounds good. | |||
| return | |||
| } | |||
| } else if b, ok := r.Body.([]byte); ok { | |||
| bodyBytes = b | |||
| } else if s, ok := r.Body.(string); ok { | |||
| bodyBytes = []byte(s) | |||
| } else if IsJSONType(contentType) && | |||
| (kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) { | |||
| bodyBytes, err = jsonMarshal(c, r, r.Body) | |||
| } else if IsXMLType(contentType) && (kind == reflect.Struct) { | |||
| bodyBytes, err = xml.Marshal(r.Body) | |||
| } | |||
| if bodyBytes == nil && r.bodyBuf == nil { | |||
| err = errors.New("unsupported 'Body' type/value") | |||
| } | |||
| // if any errors during body bytes handling, return it | |||
| if err != nil { | |||
| return | |||
| } | |||
| // []byte into Buffer | |||
| if bodyBytes != nil && r.bodyBuf == nil { | |||
| r.bodyBuf = acquireBuffer() | |||
| _, _ = r.bodyBuf.Write(bodyBytes) | |||
| } | |||
| return | |||
| } | |||
| func saveResponseIntoFile(c *Client, res *Response) error { | |||
| if res.Request.isSaveResponse { | |||
| file := "" | |||
| if len(c.outputDirectory) > 0 && !filepath.IsAbs(res.Request.outputFile) { | |||
| file += c.outputDirectory + string(filepath.Separator) | |||
| } | |||
| file = filepath.Clean(file + res.Request.outputFile) | |||
| if err := createDirectory(filepath.Dir(file)); err != nil { | |||
| return err | |||
| } | |||
| outFile, err := os.Create(file) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| defer closeq(outFile) | |||
| // io.Copy reads maximum 32kb size, it is perfect for large file download too | |||
| defer closeq(res.RawResponse.Body) | |||
| written, err := io.Copy(outFile, res.RawResponse.Body) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| res.size = written | |||
| } | |||
| return nil | |||
| } | |||
| @@ -0,0 +1,101 @@ | |||
| // Copyright (c) 2015-2020 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. | |||
| // resty source code and usage is governed by a MIT style | |||
| // license that can be found in the LICENSE file. | |||
| package resty | |||
| import ( | |||
| "errors" | |||
| "fmt" | |||
| "net" | |||
| "net/http" | |||
| "strings" | |||
| ) | |||
| type ( | |||
| // RedirectPolicy to regulate the redirects in the resty client. | |||
| // Objects implementing the RedirectPolicy interface can be registered as | |||
| // | |||
| // Apply function should return nil to continue the redirect jounery, otherwise | |||
| // return error to stop the redirect. | |||
| RedirectPolicy interface { | |||
| Apply(req *http.Request, via []*http.Request) error | |||
| } | |||
| // The RedirectPolicyFunc type is an adapter to allow the use of ordinary functions as RedirectPolicy. | |||
| // If f is a function with the appropriate signature, RedirectPolicyFunc(f) is a RedirectPolicy object that calls f. | |||
| RedirectPolicyFunc func(*http.Request, []*http.Request) error | |||
| ) | |||
| // Apply calls f(req, via). | |||
| func (f RedirectPolicyFunc) Apply(req *http.Request, via []*http.Request) error { | |||
| return f(req, via) | |||
| } | |||
| // NoRedirectPolicy is used to disable redirects in the HTTP client | |||
| // resty.SetRedirectPolicy(NoRedirectPolicy()) | |||
| func NoRedirectPolicy() RedirectPolicy { | |||
| return RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error { | |||
| return errors.New("auto redirect is disabled") | |||
| }) | |||
| } | |||
| // FlexibleRedirectPolicy is convenient method to create No of redirect policy for HTTP client. | |||
| // resty.SetRedirectPolicy(FlexibleRedirectPolicy(20)) | |||
| func FlexibleRedirectPolicy(noOfRedirect int) RedirectPolicy { | |||
| return RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error { | |||
| if len(via) >= noOfRedirect { | |||
| return fmt.Errorf("stopped after %d redirects", noOfRedirect) | |||
| } | |||
| checkHostAndAddHeaders(req, via[0]) | |||
| return nil | |||
| }) | |||
| } | |||
| // DomainCheckRedirectPolicy is convenient method to define domain name redirect rule in resty client. | |||
| // Redirect is allowed for only mentioned host in the policy. | |||
| // resty.SetRedirectPolicy(DomainCheckRedirectPolicy("host1.com", "host2.org", "host3.net")) | |||
| func DomainCheckRedirectPolicy(hostnames ...string) RedirectPolicy { | |||
| hosts := make(map[string]bool) | |||
| for _, h := range hostnames { | |||
| hosts[strings.ToLower(h)] = true | |||
| } | |||
| fn := RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error { | |||
| if ok := hosts[getHostname(req.URL.Host)]; !ok { | |||
| return errors.New("redirect is not allowed as per DomainCheckRedirectPolicy") | |||
| } | |||
| return nil | |||
| }) | |||
| return fn | |||
| } | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // Package Unexported methods | |||
| //_______________________________________________________________________ | |||
| func getHostname(host string) (hostname string) { | |||
| if strings.Index(host, ":") > 0 { | |||
| host, _, _ = net.SplitHostPort(host) | |||
| } | |||
| hostname = strings.ToLower(host) | |||
| return | |||
| } | |||
| // By default Golang will not redirect request headers | |||
| // after go throughing various discussion comments from thread | |||
| // https://github.com/golang/go/issues/4800 | |||
| // Resty will add all the headers during a redirect for the same host | |||
| func checkHostAndAddHeaders(cur *http.Request, pre *http.Request) { | |||
| curHostname := getHostname(cur.URL.Host) | |||
| preHostname := getHostname(pre.URL.Host) | |||
| if strings.EqualFold(curHostname, preHostname) { | |||
| for key, val := range pre.Header { | |||
| cur.Header[key] = val | |||
| } | |||
| } else { // only library User-Agent header is added | |||
| cur.Header.Set(hdrUserAgentKey, hdrUserAgentValue) | |||
| } | |||
| } | |||
| @@ -0,0 +1,809 @@ | |||
| // Copyright (c) 2015-2020 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. | |||
| // resty source code and usage is governed by a MIT style | |||
| // license that can be found in the LICENSE file. | |||
| package resty | |||
| import ( | |||
| "bytes" | |||
| "context" | |||
| "encoding/json" | |||
| "encoding/xml" | |||
| "fmt" | |||
| "io" | |||
| "net" | |||
| "net/http" | |||
| "net/url" | |||
| "reflect" | |||
| "strings" | |||
| "time" | |||
| ) | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // Request struct and methods | |||
| //_______________________________________________________________________ | |||
| // Request struct is used to compose and fire individual request from | |||
| // resty client. Request provides an options to override client level | |||
| // settings and also an options for the request composition. | |||
| type Request struct { | |||
| URL string | |||
| Method string | |||
| Token string | |||
| AuthScheme string | |||
| QueryParam url.Values | |||
| FormData url.Values | |||
| Header http.Header | |||
| Time time.Time | |||
| Body interface{} | |||
| Result interface{} | |||
| Error interface{} | |||
| RawRequest *http.Request | |||
| SRV *SRVRecord | |||
| UserInfo *User | |||
| Cookies []*http.Cookie | |||
| isMultiPart bool | |||
| isFormData bool | |||
| setContentLength bool | |||
| isSaveResponse bool | |||
| notParseResponse bool | |||
| jsonEscapeHTML bool | |||
| trace bool | |||
| outputFile string | |||
| fallbackContentType string | |||
| forceContentType string | |||
| ctx context.Context | |||
| pathParams map[string]string | |||
| values map[string]interface{} | |||
| client *Client | |||
| bodyBuf *bytes.Buffer | |||
| clientTrace *clientTrace | |||
| multipartFiles []*File | |||
| multipartFields []*MultipartField | |||
| } | |||
| // Context method returns the Context if its already set in request | |||
| // otherwise it creates new one using `context.Background()`. | |||
| func (r *Request) Context() context.Context { | |||
| if r.ctx == nil { | |||
| return context.Background() | |||
| } | |||
| return r.ctx | |||
| } | |||
| // SetContext method sets the context.Context for current Request. It allows | |||
| // to interrupt the request execution if ctx.Done() channel is closed. | |||
| // See https://blog.golang.org/context article and the "context" package | |||
| // documentation. | |||
| func (r *Request) SetContext(ctx context.Context) *Request { | |||
| r.ctx = ctx | |||
| return r | |||
| } | |||
| // SetHeader method is to set a single header field and its value in the current request. | |||
| // | |||
| // For Example: To set `Content-Type` and `Accept` as `application/json`. | |||
| // client.R(). | |||
| // SetHeader("Content-Type", "application/json"). | |||
| // SetHeader("Accept", "application/json") | |||
| // | |||
| // Also you can override header value, which was set at client instance level. | |||
| func (r *Request) SetHeader(header, value string) *Request { | |||
| r.Header.Set(header, value) | |||
| return r | |||
| } | |||
| // SetHeaders method sets multiple headers field and its values at one go in the current request. | |||
| // | |||
| // For Example: To set `Content-Type` and `Accept` as `application/json` | |||
| // | |||
| // client.R(). | |||
| // SetHeaders(map[string]string{ | |||
| // "Content-Type": "application/json", | |||
| // "Accept": "application/json", | |||
| // }) | |||
| // Also you can override header value, which was set at client instance level. | |||
| func (r *Request) SetHeaders(headers map[string]string) *Request { | |||
| for h, v := range headers { | |||
| r.SetHeader(h, v) | |||
| } | |||
| return r | |||
| } | |||
| // SetQueryParam method sets single parameter and its value in the current request. | |||
| // It will be formed as query string for the request. | |||
| // | |||
| // For Example: `search=kitchen%20papers&size=large` in the URL after `?` mark. | |||
| // client.R(). | |||
| // SetQueryParam("search", "kitchen papers"). | |||
| // SetQueryParam("size", "large") | |||
| // Also you can override query params value, which was set at client instance level. | |||
| func (r *Request) SetQueryParam(param, value string) *Request { | |||
| r.QueryParam.Set(param, value) | |||
| return r | |||
| } | |||
| // SetQueryParams method sets multiple parameters and its values at one go in the current request. | |||
| // It will be formed as query string for the request. | |||
| // | |||
| // For Example: `search=kitchen%20papers&size=large` in the URL after `?` mark. | |||
| // client.R(). | |||
| // SetQueryParams(map[string]string{ | |||
| // "search": "kitchen papers", | |||
| // "size": "large", | |||
| // }) | |||
| // Also you can override query params value, which was set at client instance level. | |||
| func (r *Request) SetQueryParams(params map[string]string) *Request { | |||
| for p, v := range params { | |||
| r.SetQueryParam(p, v) | |||
| } | |||
| return r | |||
| } | |||
| // SetQueryParamsFromValues method appends multiple parameters with multi-value | |||
| // (`url.Values`) at one go in the current request. It will be formed as | |||
| // query string for the request. | |||
| // | |||
| // For Example: `status=pending&status=approved&status=open` in the URL after `?` mark. | |||
| // client.R(). | |||
| // SetQueryParamsFromValues(url.Values{ | |||
| // "status": []string{"pending", "approved", "open"}, | |||
| // }) | |||
| // Also you can override query params value, which was set at client instance level. | |||
| func (r *Request) SetQueryParamsFromValues(params url.Values) *Request { | |||
| for p, v := range params { | |||
| for _, pv := range v { | |||
| r.QueryParam.Add(p, pv) | |||
| } | |||
| } | |||
| return r | |||
| } | |||
| // SetQueryString method provides ability to use string as an input to set URL query string for the request. | |||
| // | |||
| // Using String as an input | |||
| // client.R(). | |||
| // SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more") | |||
| func (r *Request) SetQueryString(query string) *Request { | |||
| params, err := url.ParseQuery(strings.TrimSpace(query)) | |||
| if err == nil { | |||
| for p, v := range params { | |||
| for _, pv := range v { | |||
| r.QueryParam.Add(p, pv) | |||
| } | |||
| } | |||
| } else { | |||
| r.client.log.Errorf("%v", err) | |||
| } | |||
| return r | |||
| } | |||
| // SetFormData method sets Form parameters and their values in the current request. | |||
| // It's applicable only HTTP method `POST` and `PUT` and requests content type would be set as | |||
| // `application/x-www-form-urlencoded`. | |||
| // client.R(). | |||
| // SetFormData(map[string]string{ | |||
| // "access_token": "BC594900-518B-4F7E-AC75-BD37F019E08F", | |||
| // "user_id": "3455454545", | |||
| // }) | |||
| // Also you can override form data value, which was set at client instance level. | |||
| func (r *Request) SetFormData(data map[string]string) *Request { | |||
| for k, v := range data { | |||
| r.FormData.Set(k, v) | |||
| } | |||
| return r | |||
| } | |||
| // SetFormDataFromValues method appends multiple form parameters with multi-value | |||
| // (`url.Values`) at one go in the current request. | |||
| // client.R(). | |||
| // SetFormDataFromValues(url.Values{ | |||
| // "search_criteria": []string{"book", "glass", "pencil"}, | |||
| // }) | |||
| // Also you can override form data value, which was set at client instance level. | |||
| func (r *Request) SetFormDataFromValues(data url.Values) *Request { | |||
| for k, v := range data { | |||
| for _, kv := range v { | |||
| r.FormData.Add(k, kv) | |||
| } | |||
| } | |||
| return r | |||
| } | |||
| // SetBody method sets the request body for the request. It supports various realtime needs as easy. | |||
| // We can say its quite handy or powerful. Supported request body data types is `string`, | |||
| // `[]byte`, `struct`, `map`, `slice` and `io.Reader`. Body value can be pointer or non-pointer. | |||
| // Automatic marshalling for JSON and XML content type, if it is `struct`, `map`, or `slice`. | |||
| // | |||
| // Note: `io.Reader` is processed as bufferless mode while sending request. | |||
| // | |||
| // For Example: Struct as a body input, based on content type, it will be marshalled. | |||
| // client.R(). | |||
| // SetBody(User{ | |||
| // Username: "jeeva@myjeeva.com", | |||
| // Password: "welcome2resty", | |||
| // }) | |||
| // | |||
| // Map as a body input, based on content type, it will be marshalled. | |||
| // client.R(). | |||
| // SetBody(map[string]interface{}{ | |||
| // "username": "jeeva@myjeeva.com", | |||
| // "password": "welcome2resty", | |||
| // "address": &Address{ | |||
| // Address1: "1111 This is my street", | |||
| // Address2: "Apt 201", | |||
| // City: "My City", | |||
| // State: "My State", | |||
| // ZipCode: 00000, | |||
| // }, | |||
| // }) | |||
| // | |||
| // String as a body input. Suitable for any need as a string input. | |||
| // client.R(). | |||
| // SetBody(`{ | |||
| // "username": "jeeva@getrightcare.com", | |||
| // "password": "admin" | |||
| // }`) | |||
| // | |||
| // []byte as a body input. Suitable for raw request such as file upload, serialize & deserialize, etc. | |||
| // client.R(). | |||
| // SetBody([]byte("This is my raw request, sent as-is")) | |||
| func (r *Request) SetBody(body interface{}) *Request { | |||
| r.Body = body | |||
| return r | |||
| } | |||
| // SetResult method is to register the response `Result` object for automatic unmarshalling for the request, | |||
| // if response status code is between 200 and 299 and content type either JSON or XML. | |||
| // | |||
| // Note: Result object can be pointer or non-pointer. | |||
| // client.R().SetResult(&AuthToken{}) | |||
| // // OR | |||
| // client.R().SetResult(AuthToken{}) | |||
| // | |||
| // Accessing a result value from response instance. | |||
| // response.Result().(*AuthToken) | |||
| func (r *Request) SetResult(res interface{}) *Request { | |||
| r.Result = getPointer(res) | |||
| return r | |||
| } | |||
| // SetError method is to register the request `Error` object for automatic unmarshalling for the request, | |||
| // if response status code is greater than 399 and content type either JSON or XML. | |||
| // | |||
| // Note: Error object can be pointer or non-pointer. | |||
| // client.R().SetError(&AuthError{}) | |||
| // // OR | |||
| // client.R().SetError(AuthError{}) | |||
| // | |||
| // Accessing a error value from response instance. | |||
| // response.Error().(*AuthError) | |||
| func (r *Request) SetError(err interface{}) *Request { | |||
| r.Error = getPointer(err) | |||
| return r | |||
| } | |||
| // SetFile method is to set single file field name and its path for multipart upload. | |||
| // client.R(). | |||
| // SetFile("my_file", "/Users/jeeva/Gas Bill - Sep.pdf") | |||
| func (r *Request) SetFile(param, filePath string) *Request { | |||
| r.isMultiPart = true | |||
| r.FormData.Set("@"+param, filePath) | |||
| return r | |||
| } | |||
| // SetFiles method is to set multiple file field name and its path for multipart upload. | |||
| // client.R(). | |||
| // SetFiles(map[string]string{ | |||
| // "my_file1": "/Users/jeeva/Gas Bill - Sep.pdf", | |||
| // "my_file2": "/Users/jeeva/Electricity Bill - Sep.pdf", | |||
| // "my_file3": "/Users/jeeva/Water Bill - Sep.pdf", | |||
| // }) | |||
| func (r *Request) SetFiles(files map[string]string) *Request { | |||
| r.isMultiPart = true | |||
| for f, fp := range files { | |||
| r.FormData.Set("@"+f, fp) | |||
| } | |||
| return r | |||
| } | |||
| // SetFileReader method is to set single file using io.Reader for multipart upload. | |||
| // client.R(). | |||
| // SetFileReader("profile_img", "my-profile-img.png", bytes.NewReader(profileImgBytes)). | |||
| // SetFileReader("notes", "user-notes.txt", bytes.NewReader(notesBytes)) | |||
| func (r *Request) SetFileReader(param, fileName string, reader io.Reader) *Request { | |||
| r.isMultiPart = true | |||
| r.multipartFiles = append(r.multipartFiles, &File{ | |||
| Name: fileName, | |||
| ParamName: param, | |||
| Reader: reader, | |||
| }) | |||
| return r | |||
| } | |||
| // SetMultipartFormData method allows simple form data to be attached to the request as `multipart:form-data` | |||
| func (r *Request) SetMultipartFormData(data map[string]string) *Request { | |||
| for k, v := range data { | |||
| r = r.SetMultipartField(k, "", "", strings.NewReader(v)) | |||
| } | |||
| return r | |||
| } | |||
| // SetMultipartField method is to set custom data using io.Reader for multipart upload. | |||
| func (r *Request) SetMultipartField(param, fileName, contentType string, reader io.Reader) *Request { | |||
| r.isMultiPart = true | |||
| r.multipartFields = append(r.multipartFields, &MultipartField{ | |||
| Param: param, | |||
| FileName: fileName, | |||
| ContentType: contentType, | |||
| Reader: reader, | |||
| }) | |||
| return r | |||
| } | |||
| // SetMultipartFields method is to set multiple data fields using io.Reader for multipart upload. | |||
| // | |||
| // For Example: | |||
| // client.R().SetMultipartFields( | |||
| // &resty.MultipartField{ | |||
| // Param: "uploadManifest1", | |||
| // FileName: "upload-file-1.json", | |||
| // ContentType: "application/json", | |||
| // Reader: strings.NewReader(`{"input": {"name": "Uploaded document 1", "_filename" : ["file1.txt"]}}`), | |||
| // }, | |||
| // &resty.MultipartField{ | |||
| // Param: "uploadManifest2", | |||
| // FileName: "upload-file-2.json", | |||
| // ContentType: "application/json", | |||
| // Reader: strings.NewReader(`{"input": {"name": "Uploaded document 2", "_filename" : ["file2.txt"]}}`), | |||
| // }) | |||
| // | |||
| // If you have slice already, then simply call- | |||
| // client.R().SetMultipartFields(fields...) | |||
| func (r *Request) SetMultipartFields(fields ...*MultipartField) *Request { | |||
| r.isMultiPart = true | |||
| r.multipartFields = append(r.multipartFields, fields...) | |||
| return r | |||
| } | |||
| // SetContentLength method sets the HTTP header `Content-Length` value for current request. | |||
| // By default Resty won't set `Content-Length`. Also you have an option to enable for every | |||
| // request. | |||
| // | |||
| // See `Client.SetContentLength` | |||
| // client.R().SetContentLength(true) | |||
| func (r *Request) SetContentLength(l bool) *Request { | |||
| r.setContentLength = true | |||
| return r | |||
| } | |||
| // SetBasicAuth method sets the basic authentication header in the current HTTP request. | |||
| // | |||
| // For Example: | |||
| // Authorization: Basic <base64-encoded-value> | |||
| // | |||
| // To set the header for username "go-resty" and password "welcome" | |||
| // client.R().SetBasicAuth("go-resty", "welcome") | |||
| // | |||
| // This method overrides the credentials set by method `Client.SetBasicAuth`. | |||
| func (r *Request) SetBasicAuth(username, password string) *Request { | |||
| r.UserInfo = &User{Username: username, Password: password} | |||
| return r | |||
| } | |||
| // SetAuthToken method sets the auth token header(Default Scheme: Bearer) in the current HTTP request. Header example: | |||
| // Authorization: Bearer <auth-token-value-comes-here> | |||
| // | |||
| // For Example: To set auth token BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F | |||
| // | |||
| // client.R().SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F") | |||
| // | |||
| // This method overrides the Auth token set by method `Client.SetAuthToken`. | |||
| func (r *Request) SetAuthToken(token string) *Request { | |||
| r.Token = token | |||
| return r | |||
| } | |||
| // SetAuthScheme method sets the auth token scheme type in the HTTP request. For Example: | |||
| // Authorization: <auth-scheme-value-set-here> <auth-token-value> | |||
| // | |||
| // For Example: To set the scheme to use OAuth | |||
| // | |||
| // client.R().SetAuthScheme("OAuth") | |||
| // | |||
| // This auth header scheme gets added to all the request rasied from this client instance. | |||
| // Also it can be overridden or set one at the request level is supported. | |||
| // | |||
| // Information about Auth schemes can be found in RFC7235 which is linked to below along with the page containing | |||
| // the currently defined official authentication schemes: | |||
| // https://tools.ietf.org/html/rfc7235 | |||
| // https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml#authschemes | |||
| // | |||
| // This method overrides the Authorization scheme set by method `Client.SetAuthScheme`. | |||
| func (r *Request) SetAuthScheme(scheme string) *Request { | |||
| r.AuthScheme = scheme | |||
| return r | |||
| } | |||
| // SetOutput method sets the output file for current HTTP request. Current HTTP response will be | |||
| // saved into given file. It is similar to `curl -o` flag. Absolute path or relative path can be used. | |||
| // If is it relative path then output file goes under the output directory, as mentioned | |||
| // in the `Client.SetOutputDirectory`. | |||
| // client.R(). | |||
| // SetOutput("/Users/jeeva/Downloads/ReplyWithHeader-v5.1-beta.zip"). | |||
| // Get("http://bit.ly/1LouEKr") | |||
| // | |||
| // Note: In this scenario `Response.Body` might be nil. | |||
| func (r *Request) SetOutput(file string) *Request { | |||
| r.outputFile = file | |||
| r.isSaveResponse = true | |||
| return r | |||
| } | |||
| // SetSRV method sets the details to query the service SRV record and execute the | |||
| // request. | |||
| // client.R(). | |||
| // SetSRV(SRVRecord{"web", "testservice.com"}). | |||
| // Get("/get") | |||
| func (r *Request) SetSRV(srv *SRVRecord) *Request { | |||
| r.SRV = srv | |||
| return r | |||
| } | |||
| // SetDoNotParseResponse method instructs `Resty` not to parse the response body automatically. | |||
| // Resty exposes the raw response body as `io.ReadCloser`. Also do not forget to close the body, | |||
| // otherwise you might get into connection leaks, no connection reuse. | |||
| // | |||
| // Note: Response middlewares are not applicable, if you use this option. Basically you have | |||
| // taken over the control of response parsing from `Resty`. | |||
| func (r *Request) SetDoNotParseResponse(parse bool) *Request { | |||
| r.notParseResponse = parse | |||
| return r | |||
| } | |||
| // SetPathParams method sets multiple URL path key-value pairs at one go in the | |||
| // Resty current request instance. | |||
| // client.R().SetPathParams(map[string]string{ | |||
| // "userId": "sample@sample.com", | |||
| // "subAccountId": "100002", | |||
| // }) | |||
| // | |||
| // Result: | |||
| // URL - /v1/users/{userId}/{subAccountId}/details | |||
| // Composed URL - /v1/users/sample@sample.com/100002/details | |||
| // It replace the value of the key while composing request URL. Also you can | |||
| // override Path Params value, which was set at client instance level. | |||
| func (r *Request) SetPathParams(params map[string]string) *Request { | |||
| for p, v := range params { | |||
| r.pathParams[p] = v | |||
| } | |||
| return r | |||
| } | |||
| // ExpectContentType method allows to provide fallback `Content-Type` for automatic unmarshalling | |||
| // when `Content-Type` response header is unavailable. | |||
| func (r *Request) ExpectContentType(contentType string) *Request { | |||
| r.fallbackContentType = contentType | |||
| return r | |||
| } | |||
| // ForceContentType method provides a strong sense of response `Content-Type` for automatic unmarshalling. | |||
| // Resty will respect it with higher priority; even response `Content-Type` response header value is available. | |||
| func (r *Request) ForceContentType(contentType string) *Request { | |||
| r.forceContentType = contentType | |||
| return r | |||
| } | |||
| // SetJSONEscapeHTML method is to enable/disable the HTML escape on JSON marshal. | |||
| // | |||
| // Note: This option only applicable to standard JSON Marshaller. | |||
| func (r *Request) SetJSONEscapeHTML(b bool) *Request { | |||
| r.jsonEscapeHTML = b | |||
| return r | |||
| } | |||
| // SetCookie method appends a single cookie in the current request instance. | |||
| // client.R().SetCookie(&http.Cookie{ | |||
| // Name:"go-resty", | |||
| // Value:"This is cookie value", | |||
| // }) | |||
| // | |||
| // Note: Method appends the Cookie value into existing Cookie if already existing. | |||
| // | |||
| // Since v2.1.0 | |||
| func (r *Request) SetCookie(hc *http.Cookie) *Request { | |||
| r.Cookies = append(r.Cookies, hc) | |||
| return r | |||
| } | |||
| // SetCookies method sets an array of cookies in the current request instance. | |||
| // cookies := []*http.Cookie{ | |||
| // &http.Cookie{ | |||
| // Name:"go-resty-1", | |||
| // Value:"This is cookie 1 value", | |||
| // }, | |||
| // &http.Cookie{ | |||
| // Name:"go-resty-2", | |||
| // Value:"This is cookie 2 value", | |||
| // }, | |||
| // } | |||
| // | |||
| // // Setting a cookies into resty's current request | |||
| // client.R().SetCookies(cookies) | |||
| // | |||
| // Note: Method appends the Cookie value into existing Cookie if already existing. | |||
| // | |||
| // Since v2.1.0 | |||
| func (r *Request) SetCookies(rs []*http.Cookie) *Request { | |||
| r.Cookies = append(r.Cookies, rs...) | |||
| return r | |||
| } | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // HTTP request tracing | |||
| //_______________________________________________________________________ | |||
| // EnableTrace method enables trace for the current request | |||
| // using `httptrace.ClientTrace` and provides insights. | |||
| // | |||
| // client := resty.New() | |||
| // | |||
| // resp, err := client.R().EnableTrace().Get("https://httpbin.org/get") | |||
| // fmt.Println("Error:", err) | |||
| // fmt.Println("Trace Info:", resp.Request.TraceInfo()) | |||
| // | |||
| // See `Client.EnableTrace` available too to get trace info for all requests. | |||
| // | |||
| // Since v2.0.0 | |||
| func (r *Request) EnableTrace() *Request { | |||
| r.trace = true | |||
| return r | |||
| } | |||
| // TraceInfo method returns the trace info for the request. | |||
| // If either the Client or Request EnableTrace function has not been called | |||
| // prior to the request being made, an empty TraceInfo object will be returned. | |||
| // | |||
| // Since v2.0.0 | |||
| func (r *Request) TraceInfo() TraceInfo { | |||
| ct := r.clientTrace | |||
| if ct == nil { | |||
| return TraceInfo{} | |||
| } | |||
| ti := TraceInfo{ | |||
| DNSLookup: ct.dnsDone.Sub(ct.dnsStart), | |||
| TLSHandshake: ct.tlsHandshakeDone.Sub(ct.tlsHandshakeStart), | |||
| ServerTime: ct.gotFirstResponseByte.Sub(ct.gotConn), | |||
| TotalTime: ct.endTime.Sub(ct.dnsStart), | |||
| IsConnReused: ct.gotConnInfo.Reused, | |||
| IsConnWasIdle: ct.gotConnInfo.WasIdle, | |||
| ConnIdleTime: ct.gotConnInfo.IdleTime, | |||
| } | |||
| // Only calcuate on successful connections | |||
| if !ct.connectDone.IsZero() { | |||
| ti.TCPConnTime = ct.connectDone.Sub(ct.dnsDone) | |||
| } | |||
| // Only calcuate on successful connections | |||
| if !ct.gotConn.IsZero() { | |||
| ti.ConnTime = ct.gotConn.Sub(ct.getConn) | |||
| } | |||
| // Only calcuate on successful connections | |||
| if !ct.gotFirstResponseByte.IsZero() { | |||
| ti.ResponseTime = ct.endTime.Sub(ct.gotFirstResponseByte) | |||
| } | |||
| return ti | |||
| } | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // HTTP verb method starts here | |||
| //_______________________________________________________________________ | |||
| // Get method does GET HTTP request. It's defined in section 4.3.1 of RFC7231. | |||
| func (r *Request) Get(url string) (*Response, error) { | |||
| return r.Execute(MethodGet, url) | |||
| } | |||
| // Head method does HEAD HTTP request. It's defined in section 4.3.2 of RFC7231. | |||
| func (r *Request) Head(url string) (*Response, error) { | |||
| return r.Execute(MethodHead, url) | |||
| } | |||
| // Post method does POST HTTP request. It's defined in section 4.3.3 of RFC7231. | |||
| func (r *Request) Post(url string) (*Response, error) { | |||
| return r.Execute(MethodPost, url) | |||
| } | |||
| // Put method does PUT HTTP request. It's defined in section 4.3.4 of RFC7231. | |||
| func (r *Request) Put(url string) (*Response, error) { | |||
| return r.Execute(MethodPut, url) | |||
| } | |||
| // Delete method does DELETE HTTP request. It's defined in section 4.3.5 of RFC7231. | |||
| func (r *Request) Delete(url string) (*Response, error) { | |||
| return r.Execute(MethodDelete, url) | |||
| } | |||
| // Options method does OPTIONS HTTP request. It's defined in section 4.3.7 of RFC7231. | |||
| func (r *Request) Options(url string) (*Response, error) { | |||
| return r.Execute(MethodOptions, url) | |||
| } | |||
| // Patch method does PATCH HTTP request. It's defined in section 2 of RFC5789. | |||
| func (r *Request) Patch(url string) (*Response, error) { | |||
| return r.Execute(MethodPatch, url) | |||
| } | |||
| // Send method performs the HTTP request using the method and URL already defined | |||
| // for current `Request`. | |||
| // req := client.R() | |||
| // req.Method = resty.GET | |||
| // req.URL = "http://httpbin.org/get" | |||
| // resp, err := client.R().Send() | |||
| func (r *Request) Send() (*Response, error) { | |||
| return r.Execute(r.Method, r.URL) | |||
| } | |||
| // Execute method performs the HTTP request with given HTTP method and URL | |||
| // for current `Request`. | |||
| // resp, err := client.R().Execute(resty.GET, "http://httpbin.org/get") | |||
| func (r *Request) Execute(method, url string) (*Response, error) { | |||
| var addrs []*net.SRV | |||
| var resp *Response | |||
| var err error | |||
| if r.isMultiPart && !(method == MethodPost || method == MethodPut || method == MethodPatch) { | |||
| return nil, fmt.Errorf("multipart content is not allowed in HTTP verb [%v]", method) | |||
| } | |||
| if r.SRV != nil { | |||
| _, addrs, err = net.LookupSRV(r.SRV.Service, "tcp", r.SRV.Domain) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| } | |||
| r.Method = method | |||
| r.URL = r.selectAddr(addrs, url, 0) | |||
| if r.client.RetryCount == 0 { | |||
| resp, err = r.client.execute(r) | |||
| return resp, unwrapNoRetryErr(err) | |||
| } | |||
| attempt := 0 | |||
| err = Backoff( | |||
| func() (*Response, error) { | |||
| attempt++ | |||
| r.URL = r.selectAddr(addrs, url, attempt) | |||
| resp, err = r.client.execute(r) | |||
| if err != nil { | |||
| r.client.log.Errorf("%v, Attempt %v", err, attempt) | |||
| } | |||
| return resp, err | |||
| }, | |||
| Retries(r.client.RetryCount), | |||
| WaitTime(r.client.RetryWaitTime), | |||
| MaxWaitTime(r.client.RetryMaxWaitTime), | |||
| RetryConditions(r.client.RetryConditions), | |||
| ) | |||
| return resp, unwrapNoRetryErr(err) | |||
| } | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // SRVRecord struct | |||
| //_______________________________________________________________________ | |||
| // SRVRecord struct holds the data to query the SRV record for the | |||
| // following service. | |||
| type SRVRecord struct { | |||
| Service string | |||
| Domain string | |||
| } | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // Request Unexported methods | |||
| //_______________________________________________________________________ | |||
| func (r *Request) fmtBodyString(sl int64) (body string) { | |||
| body = "***** NO CONTENT *****" | |||
| if !isPayloadSupported(r.Method, r.client.AllowGetMethodPayload) { | |||
| return | |||
| } | |||
| if _, ok := r.Body.(io.Reader); ok { | |||
| body = "***** BODY IS io.Reader *****" | |||
| return | |||
| } | |||
| // multipart or form-data | |||
| if r.isMultiPart || r.isFormData { | |||
| bodySize := int64(r.bodyBuf.Len()) | |||
| if bodySize > sl { | |||
| body = fmt.Sprintf("***** REQUEST TOO LARGE (size - %d) *****", bodySize) | |||
| return | |||
| } | |||
| body = r.bodyBuf.String() | |||
| return | |||
| } | |||
| // request body data | |||
| if r.Body == nil { | |||
| return | |||
| } | |||
| var prtBodyBytes []byte | |||
| var err error | |||
| contentType := r.Header.Get(hdrContentTypeKey) | |||
| kind := kindOf(r.Body) | |||
| if canJSONMarshal(contentType, kind) { | |||
| prtBodyBytes, err = json.MarshalIndent(&r.Body, "", " ") | |||
| } else if IsXMLType(contentType) && (kind == reflect.Struct) { | |||
| prtBodyBytes, err = xml.MarshalIndent(&r.Body, "", " ") | |||
| } else if b, ok := r.Body.(string); ok { | |||
| if IsJSONType(contentType) { | |||
| bodyBytes := []byte(b) | |||
| out := acquireBuffer() | |||
| defer releaseBuffer(out) | |||
| if err = json.Indent(out, bodyBytes, "", " "); err == nil { | |||
| prtBodyBytes = out.Bytes() | |||
| } | |||
| } else { | |||
| body = b | |||
| } | |||
| } else if b, ok := r.Body.([]byte); ok { | |||
| body = fmt.Sprintf("***** BODY IS byte(s) (size - %d) *****", len(b)) | |||
| return | |||
| } | |||
| if prtBodyBytes != nil && err == nil { | |||
| body = string(prtBodyBytes) | |||
| } | |||
| if len(body) > 0 { | |||
| bodySize := int64(len([]byte(body))) | |||
| if bodySize > sl { | |||
| body = fmt.Sprintf("***** REQUEST TOO LARGE (size - %d) *****", bodySize) | |||
| } | |||
| } | |||
| return | |||
| } | |||
| func (r *Request) selectAddr(addrs []*net.SRV, path string, attempt int) string { | |||
| if addrs == nil { | |||
| return path | |||
| } | |||
| idx := attempt % len(addrs) | |||
| domain := strings.TrimRight(addrs[idx].Target, ".") | |||
| path = strings.TrimLeft(path, "/") | |||
| return fmt.Sprintf("%s://%s:%d/%s", r.client.scheme, domain, addrs[idx].Port, path) | |||
| } | |||
| func (r *Request) initValuesMap() { | |||
| if r.values == nil { | |||
| r.values = make(map[string]interface{}) | |||
| } | |||
| } | |||
| var noescapeJSONMarshal = func(v interface{}) ([]byte, error) { | |||
| buf := acquireBuffer() | |||
| defer releaseBuffer(buf) | |||
| encoder := json.NewEncoder(buf) | |||
| encoder.SetEscapeHTML(false) | |||
| err := encoder.Encode(v) | |||
| return buf.Bytes(), err | |||
| } | |||
| @@ -0,0 +1,175 @@ | |||
| // Copyright (c) 2015-2020 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. | |||
| // resty source code and usage is governed by a MIT style | |||
| // license that can be found in the LICENSE file. | |||
| package resty | |||
| import ( | |||
| "encoding/json" | |||
| "fmt" | |||
| "io" | |||
| "net/http" | |||
| "strings" | |||
| "time" | |||
| ) | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // Response struct and methods | |||
| //_______________________________________________________________________ | |||
| // Response struct holds response values of executed request. | |||
| type Response struct { | |||
| Request *Request | |||
| RawResponse *http.Response | |||
| body []byte | |||
| size int64 | |||
| receivedAt time.Time | |||
| } | |||
| // Body method returns HTTP response as []byte array for the executed request. | |||
| // | |||
| // Note: `Response.Body` might be nil, if `Request.SetOutput` is used. | |||
| func (r *Response) Body() []byte { | |||
| if r.RawResponse == nil { | |||
| return []byte{} | |||
| } | |||
| return r.body | |||
| } | |||
| // Status method returns the HTTP status string for the executed request. | |||
| // Example: 200 OK | |||
| func (r *Response) Status() string { | |||
| if r.RawResponse == nil { | |||
| return "" | |||
| } | |||
| return r.RawResponse.Status | |||
| } | |||
| // StatusCode method returns the HTTP status code for the executed request. | |||
| // Example: 200 | |||
| func (r *Response) StatusCode() int { | |||
| if r.RawResponse == nil { | |||
| return 0 | |||
| } | |||
| return r.RawResponse.StatusCode | |||
| } | |||
| // Proto method returns the HTTP response protocol used for the request. | |||
| func (r *Response) Proto() string { | |||
| if r.RawResponse == nil { | |||
| return "" | |||
| } | |||
| return r.RawResponse.Proto | |||
| } | |||
| // Result method returns the response value as an object if it has one | |||
| func (r *Response) Result() interface{} { | |||
| return r.Request.Result | |||
| } | |||
| // Error method returns the error object if it has one | |||
| func (r *Response) Error() interface{} { | |||
| return r.Request.Error | |||
| } | |||
| // Header method returns the response headers | |||
| func (r *Response) Header() http.Header { | |||
| if r.RawResponse == nil { | |||
| return http.Header{} | |||
| } | |||
| return r.RawResponse.Header | |||
| } | |||
| // Cookies method to access all the response cookies | |||
| func (r *Response) Cookies() []*http.Cookie { | |||
| if r.RawResponse == nil { | |||
| return make([]*http.Cookie, 0) | |||
| } | |||
| return r.RawResponse.Cookies() | |||
| } | |||
| // String method returns the body of the server response as String. | |||
| func (r *Response) String() string { | |||
| if r.body == nil { | |||
| return "" | |||
| } | |||
| return strings.TrimSpace(string(r.body)) | |||
| } | |||
| // Time method returns the time of HTTP response time that from request we sent and received a request. | |||
| // | |||
| // See `Response.ReceivedAt` to know when client recevied response and see `Response.Request.Time` to know | |||
| // when client sent a request. | |||
| func (r *Response) Time() time.Duration { | |||
| if r.Request.clientTrace != nil { | |||
| return r.Request.TraceInfo().TotalTime | |||
| } | |||
| return r.receivedAt.Sub(r.Request.Time) | |||
| } | |||
| // ReceivedAt method returns when response got recevied from server for the request. | |||
| func (r *Response) ReceivedAt() time.Time { | |||
| return r.receivedAt | |||
| } | |||
| // Size method returns the HTTP response size in bytes. Ya, you can relay on HTTP `Content-Length` header, | |||
| // however it won't be good for chucked transfer/compressed response. Since Resty calculates response size | |||
| // at the client end. You will get actual size of the http response. | |||
| func (r *Response) Size() int64 { | |||
| return r.size | |||
| } | |||
| // RawBody method exposes the HTTP raw response body. Use this method in-conjunction with `SetDoNotParseResponse` | |||
| // option otherwise you get an error as `read err: http: read on closed response body`. | |||
| // | |||
| // Do not forget to close the body, otherwise you might get into connection leaks, no connection reuse. | |||
| // Basically you have taken over the control of response parsing from `Resty`. | |||
| func (r *Response) RawBody() io.ReadCloser { | |||
| if r.RawResponse == nil { | |||
| return nil | |||
| } | |||
| return r.RawResponse.Body | |||
| } | |||
| // IsSuccess method returns true if HTTP status `code >= 200 and <= 299` otherwise false. | |||
| func (r *Response) IsSuccess() bool { | |||
| return r.StatusCode() > 199 && r.StatusCode() < 300 | |||
| } | |||
| // IsError method returns true if HTTP status `code >= 400` otherwise false. | |||
| func (r *Response) IsError() bool { | |||
| return r.StatusCode() > 399 | |||
| } | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // Response Unexported methods | |||
| //_______________________________________________________________________ | |||
| func (r *Response) setReceivedAt() { | |||
| r.receivedAt = time.Now() | |||
| if r.Request.clientTrace != nil { | |||
| r.Request.clientTrace.endTime = r.receivedAt | |||
| } | |||
| } | |||
| func (r *Response) fmtBodyString(sl int64) string { | |||
| if r.body != nil { | |||
| if int64(len(r.body)) > sl { | |||
| return fmt.Sprintf("***** RESPONSE TOO LARGE (size - %d) *****", len(r.body)) | |||
| } | |||
| ct := r.Header().Get(hdrContentTypeKey) | |||
| if IsJSONType(ct) { | |||
| out := acquireBuffer() | |||
| defer releaseBuffer(out) | |||
| err := json.Indent(out, r.body, "", " ") | |||
| if err != nil { | |||
| return fmt.Sprintf("*** Error: Unable to format response body - \"%s\" ***\n\nLog Body as-is:\n%s", err, r.String()) | |||
| } | |||
| return out.String() | |||
| } | |||
| return r.String() | |||
| } | |||
| return "***** NO CONTENT *****" | |||
| } | |||
| @@ -0,0 +1,40 @@ | |||
| // Copyright (c) 2015-2020 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. | |||
| // resty source code and usage is governed by a MIT style | |||
| // license that can be found in the LICENSE file. | |||
| // Package resty provides Simple HTTP and REST client library for Go. | |||
| package resty | |||
| import ( | |||
| "net" | |||
| "net/http" | |||
| "net/http/cookiejar" | |||
| "golang.org/x/net/publicsuffix" | |||
| ) | |||
| // Version # of resty | |||
| const Version = "2.3.0" | |||
| // New method creates a new Resty client. | |||
| func New() *Client { | |||
| cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) | |||
| return createClient(&http.Client{ | |||
| Jar: cookieJar, | |||
| }) | |||
| } | |||
| // NewWithClient method creates a new Resty client with given `http.Client`. | |||
| func NewWithClient(hc *http.Client) *Client { | |||
| return createClient(hc) | |||
| } | |||
| // NewWithLocalAddr method creates a new Resty client with given Local Address | |||
| // to dial from. | |||
| func NewWithLocalAddr(localAddr net.Addr) *Client { | |||
| cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) | |||
| return createClient(&http.Client{ | |||
| Jar: cookieJar, | |||
| Transport: createTransport(localAddr), | |||
| }) | |||
| } | |||
| @@ -0,0 +1,181 @@ | |||
| // Copyright (c) 2015-2020 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. | |||
| // resty source code and usage is governed by a MIT style | |||
| // license that can be found in the LICENSE file. | |||
| package resty | |||
| import ( | |||
| "context" | |||
| "math" | |||
| "math/rand" | |||
| "time" | |||
| ) | |||
| const ( | |||
| defaultMaxRetries = 3 | |||
| defaultWaitTime = time.Duration(100) * time.Millisecond | |||
| defaultMaxWaitTime = time.Duration(2000) * time.Millisecond | |||
| ) | |||
| type ( | |||
| // Option is to create convenient retry options like wait time, max retries, etc. | |||
| Option func(*Options) | |||
| // RetryConditionFunc type is for retry condition function | |||
| // input: non-nil Response OR request execution error | |||
| RetryConditionFunc func(*Response, error) bool | |||
| // RetryAfterFunc returns time to wait before retry | |||
| // For example, it can parse HTTP Retry-After header | |||
| // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html | |||
| // Non-nil error is returned if it is found that request is not retryable | |||
| // (0, nil) is a special result means 'use default algorithm' | |||
| RetryAfterFunc func(*Client, *Response) (time.Duration, error) | |||
| // Options struct is used to hold retry settings. | |||
| Options struct { | |||
| maxRetries int | |||
| waitTime time.Duration | |||
| maxWaitTime time.Duration | |||
| retryConditions []RetryConditionFunc | |||
| } | |||
| ) | |||
| // Retries sets the max number of retries | |||
| func Retries(value int) Option { | |||
| return func(o *Options) { | |||
| o.maxRetries = value | |||
| } | |||
| } | |||
| // WaitTime sets the default wait time to sleep between requests | |||
| func WaitTime(value time.Duration) Option { | |||
| return func(o *Options) { | |||
| o.waitTime = value | |||
| } | |||
| } | |||
| // MaxWaitTime sets the max wait time to sleep between requests | |||
| func MaxWaitTime(value time.Duration) Option { | |||
| return func(o *Options) { | |||
| o.maxWaitTime = value | |||
| } | |||
| } | |||
| // RetryConditions sets the conditions that will be checked for retry. | |||
| func RetryConditions(conditions []RetryConditionFunc) Option { | |||
| return func(o *Options) { | |||
| o.retryConditions = conditions | |||
| } | |||
| } | |||
| // Backoff retries with increasing timeout duration up until X amount of retries | |||
| // (Default is 3 attempts, Override with option Retries(n)) | |||
| func Backoff(operation func() (*Response, error), options ...Option) error { | |||
| // Defaults | |||
| opts := Options{ | |||
| maxRetries: defaultMaxRetries, | |||
| waitTime: defaultWaitTime, | |||
| maxWaitTime: defaultMaxWaitTime, | |||
| retryConditions: []RetryConditionFunc{}, | |||
| } | |||
| for _, o := range options { | |||
| o(&opts) | |||
| } | |||
| var ( | |||
| resp *Response | |||
| err error | |||
| ) | |||
| for attempt := 0; attempt <= opts.maxRetries; attempt++ { | |||
| resp, err = operation() | |||
| ctx := context.Background() | |||
| if resp != nil && resp.Request.ctx != nil { | |||
| ctx = resp.Request.ctx | |||
| } | |||
| if ctx.Err() != nil { | |||
| return err | |||
| } | |||
| err1 := unwrapNoRetryErr(err) // raw error, it used for return users callback. | |||
| needsRetry := err != nil && err == err1 // retry on a few operation errors by default | |||
| for _, condition := range opts.retryConditions { | |||
| needsRetry = condition(resp, err1) | |||
| if needsRetry { | |||
| break | |||
| } | |||
| } | |||
| if !needsRetry { | |||
| return err | |||
| } | |||
| waitTime, err2 := sleepDuration(resp, opts.waitTime, opts.maxWaitTime, attempt) | |||
| if err2 != nil { | |||
| if err == nil { | |||
| err = err2 | |||
| } | |||
| return err | |||
| } | |||
| select { | |||
| case <-time.After(waitTime): | |||
| case <-ctx.Done(): | |||
| return ctx.Err() | |||
| } | |||
| } | |||
| return err | |||
| } | |||
| func sleepDuration(resp *Response, min, max time.Duration, attempt int) (time.Duration, error) { | |||
| const maxInt = 1<<31 - 1 // max int for arch 386 | |||
| if max < 0 { | |||
| max = maxInt | |||
| } | |||
| if resp == nil { | |||
| goto defaultCase | |||
| } | |||
| // 1. Check for custom callback | |||
| if retryAfterFunc := resp.Request.client.RetryAfter; retryAfterFunc != nil { | |||
| result, err := retryAfterFunc(resp.Request.client, resp) | |||
| if err != nil { | |||
| return 0, err // i.e. 'API quota exceeded' | |||
| } | |||
| if result == 0 { | |||
| goto defaultCase | |||
| } | |||
| if result < 0 || max < result { | |||
| result = max | |||
| } | |||
| if result < min { | |||
| result = min | |||
| } | |||
| return result, nil | |||
| } | |||
| // 2. Return capped exponential backoff with jitter | |||
| // http://www.awsarchitectureblog.com/2015/03/backoff.html | |||
| defaultCase: | |||
| base := float64(min) | |||
| capLevel := float64(max) | |||
| temp := math.Min(capLevel, base*math.Exp2(float64(attempt))) | |||
| ri := int(temp / 2) | |||
| if ri <= 0 { | |||
| ri = maxInt // max int for arch 386 | |||
| } | |||
| result := time.Duration(math.Abs(float64(ri + rand.Intn(ri)))) | |||
| if result < min { | |||
| result = min | |||
| } | |||
| return result, nil | |||
| } | |||
| @@ -0,0 +1,122 @@ | |||
| // Copyright (c) 2015-2020 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. | |||
| // resty source code and usage is governed by a MIT style | |||
| // license that can be found in the LICENSE file. | |||
| package resty | |||
| import ( | |||
| "context" | |||
| "crypto/tls" | |||
| "net/http/httptrace" | |||
| "time" | |||
| ) | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // TraceInfo struct | |||
| //_______________________________________________________________________ | |||
| // TraceInfo struct is used provide request trace info such as DNS lookup | |||
| // duration, Connection obtain duration, Server processing duration, etc. | |||
| // | |||
| // Since v2.0.0 | |||
| type TraceInfo struct { | |||
| // DNSLookup is a duration that transport took to perform | |||
| // DNS lookup. | |||
| DNSLookup time.Duration | |||
| // ConnTime is a duration that took to obtain a successful connection. | |||
| ConnTime time.Duration | |||
| // TCPConnTime is a duration that took to obtain the TCP connection. | |||
| TCPConnTime time.Duration | |||
| // TLSHandshake is a duration that TLS handshake took place. | |||
| TLSHandshake time.Duration | |||
| // ServerTime is a duration that server took to respond first byte. | |||
| ServerTime time.Duration | |||
| // ResponseTime is a duration since first response byte from server to | |||
| // request completion. | |||
| ResponseTime time.Duration | |||
| // TotalTime is a duration that total request took end-to-end. | |||
| TotalTime time.Duration | |||
| // IsConnReused is whether this connection has been previously | |||
| // used for another HTTP request. | |||
| IsConnReused bool | |||
| // IsConnWasIdle is whether this connection was obtained from an | |||
| // idle pool. | |||
| IsConnWasIdle bool | |||
| // ConnIdleTime is a duration how long the connection was previously | |||
| // idle, if IsConnWasIdle is true. | |||
| ConnIdleTime time.Duration | |||
| } | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // CientTrace struct and its methods | |||
| //_______________________________________________________________________ | |||
| // tracer struct maps the `httptrace.ClientTrace` hooks into Fields | |||
| // with same naming for easy understanding. Plus additional insights | |||
| // Request. | |||
| type clientTrace struct { | |||
| getConn time.Time | |||
| dnsStart time.Time | |||
| dnsDone time.Time | |||
| connectDone time.Time | |||
| tlsHandshakeStart time.Time | |||
| tlsHandshakeDone time.Time | |||
| gotConn time.Time | |||
| gotFirstResponseByte time.Time | |||
| endTime time.Time | |||
| gotConnInfo httptrace.GotConnInfo | |||
| } | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // Trace unexported methods | |||
| //_______________________________________________________________________ | |||
| func (t *clientTrace) createContext(ctx context.Context) context.Context { | |||
| return httptrace.WithClientTrace( | |||
| ctx, | |||
| &httptrace.ClientTrace{ | |||
| DNSStart: func(_ httptrace.DNSStartInfo) { | |||
| t.dnsStart = time.Now() | |||
| }, | |||
| DNSDone: func(_ httptrace.DNSDoneInfo) { | |||
| t.dnsDone = time.Now() | |||
| }, | |||
| ConnectStart: func(_, _ string) { | |||
| if t.dnsDone.IsZero() { | |||
| t.dnsDone = time.Now() | |||
| } | |||
| if t.dnsStart.IsZero() { | |||
| t.dnsStart = t.dnsDone | |||
| } | |||
| }, | |||
| ConnectDone: func(net, addr string, err error) { | |||
| t.connectDone = time.Now() | |||
| }, | |||
| GetConn: func(_ string) { | |||
| t.getConn = time.Now() | |||
| }, | |||
| GotConn: func(ci httptrace.GotConnInfo) { | |||
| t.gotConn = time.Now() | |||
| t.gotConnInfo = ci | |||
| }, | |||
| GotFirstResponseByte: func() { | |||
| t.gotFirstResponseByte = time.Now() | |||
| }, | |||
| TLSHandshakeStart: func() { | |||
| t.tlsHandshakeStart = time.Now() | |||
| }, | |||
| TLSHandshakeDone: func(_ tls.ConnectionState, _ error) { | |||
| t.tlsHandshakeDone = time.Now() | |||
| }, | |||
| }, | |||
| ) | |||
| } | |||
| @@ -0,0 +1,35 @@ | |||
| // +build go1.13 | |||
| // Copyright (c) 2015-2020 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. | |||
| // resty source code and usage is governed by a MIT style | |||
| // license that can be found in the LICENSE file. | |||
| package resty | |||
| import ( | |||
| "net" | |||
| "net/http" | |||
| "runtime" | |||
| "time" | |||
| ) | |||
| func createTransport(localAddr net.Addr) *http.Transport { | |||
| dialer := &net.Dialer{ | |||
| Timeout: 30 * time.Second, | |||
| KeepAlive: 30 * time.Second, | |||
| DualStack: true, | |||
| } | |||
| if localAddr != nil { | |||
| dialer.LocalAddr = localAddr | |||
| } | |||
| return &http.Transport{ | |||
| Proxy: http.ProxyFromEnvironment, | |||
| DialContext: dialer.DialContext, | |||
| ForceAttemptHTTP2: true, | |||
| MaxIdleConns: 100, | |||
| IdleConnTimeout: 90 * time.Second, | |||
| TLSHandshakeTimeout: 10 * time.Second, | |||
| ExpectContinueTimeout: 1 * time.Second, | |||
| MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1, | |||
| } | |||
| } | |||
| @@ -0,0 +1,34 @@ | |||
| // +build !go1.13 | |||
| // Copyright (c) 2015-2020 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. | |||
| // resty source code and usage is governed by a MIT style | |||
| // license that can be found in the LICENSE file. | |||
| package resty | |||
| import ( | |||
| "net" | |||
| "net/http" | |||
| "runtime" | |||
| "time" | |||
| ) | |||
| func createTransport(localAddr net.Addr) *http.Transport { | |||
| dialer := &net.Dialer{ | |||
| Timeout: 30 * time.Second, | |||
| KeepAlive: 30 * time.Second, | |||
| DualStack: true, | |||
| } | |||
| if localAddr != nil { | |||
| dialer.LocalAddr = localAddr | |||
| } | |||
| return &http.Transport{ | |||
| Proxy: http.ProxyFromEnvironment, | |||
| DialContext: dialer.DialContext, | |||
| MaxIdleConns: 100, | |||
| IdleConnTimeout: 90 * time.Second, | |||
| TLSHandshakeTimeout: 10 * time.Second, | |||
| ExpectContinueTimeout: 1 * time.Second, | |||
| MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1, | |||
| } | |||
| } | |||
| @@ -0,0 +1,357 @@ | |||
| // Copyright (c) 2015-2020 Jeevanandam M (jeeva@myjeeva.com), All rights reserved. | |||
| // resty source code and usage is governed by a MIT style | |||
| // license that can be found in the LICENSE file. | |||
| package resty | |||
| import ( | |||
| "bytes" | |||
| "encoding/xml" | |||
| "fmt" | |||
| "io" | |||
| "log" | |||
| "mime/multipart" | |||
| "net/http" | |||
| "net/textproto" | |||
| "os" | |||
| "path/filepath" | |||
| "reflect" | |||
| "runtime" | |||
| "sort" | |||
| "strings" | |||
| ) | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // Logger interface | |||
| //_______________________________________________________________________ | |||
| // Logger interface is to abstract the logging from Resty. Gives control to | |||
| // the Resty users, choice of the logger. | |||
| type Logger interface { | |||
| Errorf(format string, v ...interface{}) | |||
| Warnf(format string, v ...interface{}) | |||
| Debugf(format string, v ...interface{}) | |||
| } | |||
| func createLogger() *logger { | |||
| l := &logger{l: log.New(os.Stderr, "", log.Ldate|log.Lmicroseconds)} | |||
| return l | |||
| } | |||
| var _ Logger = (*logger)(nil) | |||
| type logger struct { | |||
| l *log.Logger | |||
| } | |||
| func (l *logger) Errorf(format string, v ...interface{}) { | |||
| l.output("ERROR RESTY "+format, v...) | |||
| } | |||
| func (l *logger) Warnf(format string, v ...interface{}) { | |||
| l.output("WARN RESTY "+format, v...) | |||
| } | |||
| func (l *logger) Debugf(format string, v ...interface{}) { | |||
| l.output("DEBUG RESTY "+format, v...) | |||
| } | |||
| func (l *logger) output(format string, v ...interface{}) { | |||
| if len(v) == 0 { | |||
| l.l.Print(format) | |||
| return | |||
| } | |||
| l.l.Printf(format, v...) | |||
| } | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // Package Helper methods | |||
| //_______________________________________________________________________ | |||
| // IsStringEmpty method tells whether given string is empty or not | |||
| func IsStringEmpty(str string) bool { | |||
| return len(strings.TrimSpace(str)) == 0 | |||
| } | |||
| // DetectContentType method is used to figure out `Request.Body` content type for request header | |||
| func DetectContentType(body interface{}) string { | |||
| contentType := plainTextType | |||
| kind := kindOf(body) | |||
| switch kind { | |||
| case reflect.Struct, reflect.Map: | |||
| contentType = jsonContentType | |||
| case reflect.String: | |||
| contentType = plainTextType | |||
| default: | |||
| if b, ok := body.([]byte); ok { | |||
| contentType = http.DetectContentType(b) | |||
| } else if kind == reflect.Slice { | |||
| contentType = jsonContentType | |||
| } | |||
| } | |||
| return contentType | |||
| } | |||
| // IsJSONType method is to check JSON content type or not | |||
| func IsJSONType(ct string) bool { | |||
| return jsonCheck.MatchString(ct) | |||
| } | |||
| // IsXMLType method is to check XML content type or not | |||
| func IsXMLType(ct string) bool { | |||
| return xmlCheck.MatchString(ct) | |||
| } | |||
| // Unmarshalc content into object from JSON or XML | |||
| func Unmarshalc(c *Client, ct string, b []byte, d interface{}) (err error) { | |||
| if IsJSONType(ct) { | |||
| err = c.JSONUnmarshal(b, d) | |||
| } else if IsXMLType(ct) { | |||
| err = xml.Unmarshal(b, d) | |||
| } | |||
| return | |||
| } | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // RequestLog and ResponseLog type | |||
| //_______________________________________________________________________ | |||
| // RequestLog struct is used to collected information from resty request | |||
| // instance for debug logging. It sent to request log callback before resty | |||
| // actually logs the information. | |||
| type RequestLog struct { | |||
| Header http.Header | |||
| Body string | |||
| } | |||
| // ResponseLog struct is used to collected information from resty response | |||
| // instance for debug logging. It sent to response log callback before resty | |||
| // actually logs the information. | |||
| type ResponseLog struct { | |||
| Header http.Header | |||
| Body string | |||
| } | |||
| //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
| // Package Unexported methods | |||
| //_______________________________________________________________________ | |||
| // way to disable the HTML escape as opt-in | |||
| func jsonMarshal(c *Client, r *Request, d interface{}) ([]byte, error) { | |||
| if !r.jsonEscapeHTML { | |||
| return noescapeJSONMarshal(d) | |||
| } else if !c.jsonEscapeHTML { | |||
| return noescapeJSONMarshal(d) | |||
| } | |||
| return c.JSONMarshal(d) | |||
| } | |||
| func firstNonEmpty(v ...string) string { | |||
| for _, s := range v { | |||
| if !IsStringEmpty(s) { | |||
| return s | |||
| } | |||
| } | |||
| return "" | |||
| } | |||
| var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") | |||
| func escapeQuotes(s string) string { | |||
| return quoteEscaper.Replace(s) | |||
| } | |||
| func createMultipartHeader(param, fileName, contentType string) textproto.MIMEHeader { | |||
| hdr := make(textproto.MIMEHeader) | |||
| var contentDispositionValue string | |||
| if IsStringEmpty(fileName) { | |||
| contentDispositionValue = fmt.Sprintf(`form-data; name="%s"`, param) | |||
| } else { | |||
| contentDispositionValue = fmt.Sprintf(`form-data; name="%s"; filename="%s"`, | |||
| param, escapeQuotes(fileName)) | |||
| } | |||
| hdr.Set("Content-Disposition", contentDispositionValue) | |||
| if !IsStringEmpty(contentType) { | |||
| hdr.Set(hdrContentTypeKey, contentType) | |||
| } | |||
| return hdr | |||
| } | |||
| func addMultipartFormField(w *multipart.Writer, mf *MultipartField) error { | |||
| partWriter, err := w.CreatePart(createMultipartHeader(mf.Param, mf.FileName, mf.ContentType)) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| _, err = io.Copy(partWriter, mf.Reader) | |||
| return err | |||
| } | |||
| func writeMultipartFormFile(w *multipart.Writer, fieldName, fileName string, r io.Reader) error { | |||
| // Auto detect actual multipart content type | |||
| cbuf := make([]byte, 512) | |||
| size, err := r.Read(cbuf) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| partWriter, err := w.CreatePart(createMultipartHeader(fieldName, fileName, http.DetectContentType(cbuf))) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| if _, err = partWriter.Write(cbuf[:size]); err != nil { | |||
| return err | |||
| } | |||
| _, err = io.Copy(partWriter, r) | |||
| return err | |||
| } | |||
| func addFile(w *multipart.Writer, fieldName, path string) error { | |||
| file, err := os.Open(path) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| defer closeq(file) | |||
| return writeMultipartFormFile(w, fieldName, filepath.Base(path), file) | |||
| } | |||
| func addFileReader(w *multipart.Writer, f *File) error { | |||
| return writeMultipartFormFile(w, f.ParamName, f.Name, f.Reader) | |||
| } | |||
| func getPointer(v interface{}) interface{} { | |||
| vv := valueOf(v) | |||
| if vv.Kind() == reflect.Ptr { | |||
| return v | |||
| } | |||
| return reflect.New(vv.Type()).Interface() | |||
| } | |||
| func isPayloadSupported(m string, allowMethodGet bool) bool { | |||
| return !(m == MethodHead || m == MethodOptions || (m == MethodGet && !allowMethodGet)) | |||
| } | |||
| func typeOf(i interface{}) reflect.Type { | |||
| return indirect(valueOf(i)).Type() | |||
| } | |||
| func valueOf(i interface{}) reflect.Value { | |||
| return reflect.ValueOf(i) | |||
| } | |||
| func indirect(v reflect.Value) reflect.Value { | |||
| return reflect.Indirect(v) | |||
| } | |||
| func kindOf(v interface{}) reflect.Kind { | |||
| return typeOf(v).Kind() | |||
| } | |||
| func createDirectory(dir string) (err error) { | |||
| if _, err = os.Stat(dir); err != nil { | |||
| if os.IsNotExist(err) { | |||
| if err = os.MkdirAll(dir, 0755); err != nil { | |||
| return | |||
| } | |||
| } | |||
| } | |||
| return | |||
| } | |||
| func canJSONMarshal(contentType string, kind reflect.Kind) bool { | |||
| return IsJSONType(contentType) && (kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) | |||
| } | |||
| func functionName(i interface{}) string { | |||
| return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() | |||
| } | |||
| func acquireBuffer() *bytes.Buffer { | |||
| return bufPool.Get().(*bytes.Buffer) | |||
| } | |||
| func releaseBuffer(buf *bytes.Buffer) { | |||
| if buf != nil { | |||
| buf.Reset() | |||
| bufPool.Put(buf) | |||
| } | |||
| } | |||
| func closeq(v interface{}) { | |||
| if c, ok := v.(io.Closer); ok { | |||
| silently(c.Close()) | |||
| } | |||
| } | |||
| func silently(_ ...interface{}) {} | |||
| func composeHeaders(c *Client, r *Request, hdrs http.Header) string { | |||
| str := make([]string, 0, len(hdrs)) | |||
| for _, k := range sortHeaderKeys(hdrs) { | |||
| var v string | |||
| if k == "Cookie" { | |||
| cv := strings.TrimSpace(strings.Join(hdrs[k], ", ")) | |||
| if c.GetClient().Jar != nil { | |||
| for _, c := range c.GetClient().Jar.Cookies(r.RawRequest.URL) { | |||
| if cv != "" { | |||
| cv = cv + "; " + c.String() | |||
| } else { | |||
| cv = c.String() | |||
| } | |||
| } | |||
| } | |||
| v = strings.TrimSpace(fmt.Sprintf("%25s: %s", k, cv)) | |||
| } else { | |||
| v = strings.TrimSpace(fmt.Sprintf("%25s: %s", k, strings.Join(hdrs[k], ", "))) | |||
| } | |||
| if v != "" { | |||
| str = append(str, "\t"+v) | |||
| } | |||
| } | |||
| return strings.Join(str, "\n") | |||
| } | |||
| func sortHeaderKeys(hdrs http.Header) []string { | |||
| keys := make([]string, 0, len(hdrs)) | |||
| for key := range hdrs { | |||
| keys = append(keys, key) | |||
| } | |||
| sort.Strings(keys) | |||
| return keys | |||
| } | |||
| func copyHeaders(hdrs http.Header) http.Header { | |||
| nh := http.Header{} | |||
| for k, v := range hdrs { | |||
| nh[k] = v | |||
| } | |||
| return nh | |||
| } | |||
| type noRetryErr struct { | |||
| err error | |||
| } | |||
| func (e *noRetryErr) Error() string { | |||
| return e.err.Error() | |||
| } | |||
| func wrapNoRetryErr(err error) error { | |||
| if err != nil { | |||
| err = &noRetryErr{err: err} | |||
| } | |||
| return err | |||
| } | |||
| func unwrapNoRetryErr(err error) error { | |||
| if e, ok := err.(*noRetryErr); ok { | |||
| err = e.err | |||
| } | |||
| return err | |||
| } | |||
| @@ -389,6 +389,9 @@ github.com/go-redis/redis/internal/hashtag | |||
| github.com/go-redis/redis/internal/pool | |||
| github.com/go-redis/redis/internal/proto | |||
| github.com/go-redis/redis/internal/util | |||
| # github.com/go-resty/resty/v2 v2.3.0 | |||
| ## explicit | |||
| github.com/go-resty/resty/v2 | |||
| # github.com/go-sql-driver/mysql v1.4.1 | |||
| ## explicit | |||
| github.com/go-sql-driver/mysql | |||
| @@ -863,7 +866,7 @@ golang.org/x/crypto/ssh/knownhosts | |||
| ## explicit | |||
| golang.org/x/mod/module | |||
| golang.org/x/mod/semver | |||
| # golang.org/x/net v0.0.0-20200506145744-7e3656a0809f | |||
| # golang.org/x/net v0.0.0-20200513185701-a91f0712d120 | |||
| ## explicit | |||
| golang.org/x/net/context | |||
| golang.org/x/net/context/ctxhttp | |||