mirror of
https://tvoygit.ru/Djam/abfapi.git
synced 2025-02-23 10:22:45 +00:00
add code
This commit is contained in:
parent
07ee2052c1
commit
4c086910cb
6 changed files with 926 additions and 1 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
.*~
|
||||||
|
*~
|
||||||
|
.env
|
||||||
|
cache.json
|
||||||
|
bin/*
|
|
@ -2,4 +2,4 @@
|
||||||
|
|
||||||
Пакет golang для ABF API
|
Пакет golang для ABF API
|
||||||
|
|
||||||
Пакет основан на кодовой базе (abf-console-client)[https://github.com/OpenMandrivaSoftware/abf-console-client]
|
Пакет основан на кодовой базе [abf-console-client](https://github.com/OpenMandrivaSoftware/abf-console-client)
|
||||||
|
|
629
abfapi.go
Normal file
629
abfapi.go
Normal file
|
@ -0,0 +1,629 @@
|
||||||
|
package abfapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gregjones/httpcache"
|
||||||
|
"github.com/gregjones/httpcache/diskcache"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxFileSize = 32 * 1024 * 1024
|
||||||
|
blockSize = 1024 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
var symbols = map[string][]string{
|
||||||
|
"basic": {"b", "k", "m", "g", "t"},
|
||||||
|
"basic_long": {"byte", "kilo", "mega", "giga", "tera"},
|
||||||
|
"iec": {"bi", "ki", "mi", "gi", "ti"},
|
||||||
|
"iec_long": {"byte", "kibi", "mebi", "gibi", "tebi"},
|
||||||
|
}
|
||||||
|
|
||||||
|
type AbfJson struct {
|
||||||
|
login string
|
||||||
|
password string
|
||||||
|
abfURL string
|
||||||
|
fileStoreURL string
|
||||||
|
base64AuthStr string
|
||||||
|
log *log.Logger
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAbfJson(abfURL, fileStoreURL, login, password string, logger *log.Logger) (*AbfJson, error) {
|
||||||
|
if !strings.HasPrefix(fileStoreURL, "http://") && !strings.HasPrefix(fileStoreURL, "https://") {
|
||||||
|
return nil, errors.New("file-store URL has to start with \"http(s)://\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir := filepath.Join(os.TempDir(), "abf_cache")
|
||||||
|
os.MkdirAll(cacheDir, 0755)
|
||||||
|
cache := diskcache.New(cacheDir)
|
||||||
|
transport := httpcache.NewTransport(cache)
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
lpw := fmt.Sprintf("%s:%s", login, password)
|
||||||
|
encodedLpw := base64.StdEncoding.EncodeToString([]byte(lpw))
|
||||||
|
|
||||||
|
return &AbfJson{
|
||||||
|
login: login,
|
||||||
|
password: password,
|
||||||
|
abfURL: strings.TrimSuffix(abfURL, "/"),
|
||||||
|
fileStoreURL: strings.TrimSuffix(fileStoreURL, "/"),
|
||||||
|
base64AuthStr: encodedLpw,
|
||||||
|
log: logger,
|
||||||
|
client: client,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) bytesToHuman(n int64, format string, symbols string) string {
|
||||||
|
if n < 0 {
|
||||||
|
panic(fmt.Sprintf("n < 0 (%d)", n))
|
||||||
|
}
|
||||||
|
|
||||||
|
sym := symbols
|
||||||
|
if strings.HasSuffix(sym, "_long") {
|
||||||
|
sym = sym[:len(sym)-6]
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := map[string]int64{
|
||||||
|
"k": 1 << 10,
|
||||||
|
"m": 1 << 20,
|
||||||
|
"g": 1 << 30,
|
||||||
|
"t": 1 << 40,
|
||||||
|
"ki": 1 << 10,
|
||||||
|
"mi": 1 << 20,
|
||||||
|
"gi": 1 << 30,
|
||||||
|
"ti": 1 << 40,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, suffix := range symbols {
|
||||||
|
if n >= prefix[string(suffix)] {
|
||||||
|
n /= prefix[string(suffix)]
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf(format, n, suffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(format, n, sym)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) getURLContents(path string, params url.Values, method string, body io.Reader) ([]byte, error) {
|
||||||
|
var fullURL string
|
||||||
|
if strings.HasPrefix(path, "/api/v1/upload") {
|
||||||
|
fullURL = a.fileStoreURL + path
|
||||||
|
} else {
|
||||||
|
fullURL = a.abfURL + path
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(params) > 0 {
|
||||||
|
fullURL += "?" + params.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, fullURL, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Basic "+a.base64AuthStr)
|
||||||
|
if method == http.MethodPost || method == http.MethodPut || method == http.MethodDelete {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := a.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) processResponse(data []byte) (map[string]interface{}, error) {
|
||||||
|
var response map[string]interface{}
|
||||||
|
err := json.Unmarshal(data, &response)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("internal server error: it has returned non-json data")
|
||||||
|
}
|
||||||
|
|
||||||
|
message, ok := response["message"].(string)
|
||||||
|
if ok && message != "Errors during build publishing!" && message != "Build is queued for publishing" {
|
||||||
|
return nil, fmt.Errorf(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errData, ok := response["error"].(string); ok {
|
||||||
|
return nil, fmt.Errorf(errData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errData, ok := response["Error"].(string); ok {
|
||||||
|
return nil, fmt.Errorf(errData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) GetArchitectures() (map[string]interface{}, error) {
|
||||||
|
data, err := a.getURLContents("/api/v1/arches.json", nil, http.MethodGet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) GetPlatforms(typ string) (map[string]interface{}, error) {
|
||||||
|
params := url.Values{}
|
||||||
|
if typ != "" {
|
||||||
|
params["type"] = []string{typ}
|
||||||
|
}
|
||||||
|
data, err := a.getURLContents("/api/v1/platforms.json", params, http.MethodGet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) GetPlatformByID(plID int) (map[string]interface{}, error) {
|
||||||
|
path := fmt.Sprintf("/api/v1/platforms/%d.json", plID)
|
||||||
|
data, err := a.getURLContents(path, nil, http.MethodGet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) GetUserID(username string) (map[string]interface{}, error) {
|
||||||
|
path := fmt.Sprintf("/api/v1/users/%s.json", username)
|
||||||
|
data, err := a.getURLContents(path, nil, http.MethodGet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) GetBuildPlatforms() (map[string]interface{}, error) {
|
||||||
|
data, err := a.getURLContents("/api/v1/platforms/platforms_for_build.json", nil, http.MethodGet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) GetRepositoryByID(repID int) (map[string]interface{}, error) {
|
||||||
|
path := fmt.Sprintf("/api/v1/repositories/%d.json", repID)
|
||||||
|
data, err := a.getURLContents(path, nil, http.MethodGet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) GetBuildListByID(blID int) (map[string]interface{}, error) {
|
||||||
|
path := fmt.Sprintf("/api/v1/build_lists/%d.json", blID)
|
||||||
|
data, err := a.getURLContents(path, nil, http.MethodGet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) GetListBuildLists(prjID int, filterQuery url.Values, page int) (map[string]interface{}, error) {
|
||||||
|
filterQuery.Set("page", fmt.Sprintf("%d", page))
|
||||||
|
filterQuery.Set("per_page", "10")
|
||||||
|
path := fmt.Sprintf("/api/v1/build_lists.json")
|
||||||
|
data, err := a.getURLContents(path, filterQuery, http.MethodGet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) GetProjectBuildLists(prjID int, filterQuery url.Values, page int) (map[string]interface{}, error) {
|
||||||
|
filterQuery.Set("page", fmt.Sprintf("%d", page))
|
||||||
|
filterQuery.Set("per_page", "10")
|
||||||
|
path := fmt.Sprintf("/api/v1/projects/%d/build_lists.json", prjID)
|
||||||
|
data, err := a.getURLContents(path, filterQuery, http.MethodGet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) GetProjectByID(pID int) (map[string]interface{}, error) {
|
||||||
|
path := fmt.Sprintf("/api/v1/projects/%d.json", pID)
|
||||||
|
data, err := a.getURLContents(path, nil, http.MethodGet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) GetProjectIDByName(key [2]string) (map[string]interface{}, error) {
|
||||||
|
params := url.Values{"name": {key[1]}, "owner": {key[0]}}
|
||||||
|
data, err := a.getURLContents("/api/v1/projects/get_id.json", params, http.MethodGet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) NewBuildTask(data map[string]interface{}) (map[string]interface{}, error) {
|
||||||
|
jsonData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dataBytes, err := a.getURLContents("/api/v1/build_lists.json", nil, http.MethodPost, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(dataBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) Publish(taskID int) (map[string]interface{}, error) {
|
||||||
|
path := fmt.Sprintf("/api/v1/build_lists/%d/publish.json", taskID)
|
||||||
|
jsonData, err := json.Marshal(map[string]interface{}{"nothing": 1})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dataBytes, err := a.getURLContents(path, nil, http.MethodPut, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(dataBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) NewPullRequest(data map[string]interface{}, pID int) (map[string]interface{}, error) {
|
||||||
|
path := fmt.Sprintf("/api/v1/projects/%d/pull_requests.json", pID)
|
||||||
|
jsonData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dataBytes, err := a.getURLContents(path, nil, http.MethodPost, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(dataBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) UpdateProject(data map[string]interface{}, pID int) (map[string]interface{}, error) {
|
||||||
|
path := fmt.Sprintf("/api/v1/projects/%d.json", pID)
|
||||||
|
jsonData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dataBytes, err := a.getURLContents(path, nil, http.MethodPut, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(dataBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) RemoveProjectFromRepo(data map[string]interface{}, repoID int) (map[string]interface{}, error) {
|
||||||
|
path := fmt.Sprintf("/api/v1/repositories/%d/remove_project.json", repoID)
|
||||||
|
jsonData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dataBytes, err := a.getURLContents(path, nil, http.MethodDelete, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(dataBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) ForkProject(data map[string]interface{}, projID int) (map[string]interface{}, error) {
|
||||||
|
path := fmt.Sprintf("/api/v1/projects/%d/fork.json", projID)
|
||||||
|
jsonData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dataBytes, err := a.getURLContents(path, nil, http.MethodPost, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(dataBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) AliasProject(data map[string]interface{}, projID int) (map[string]interface{}, error) {
|
||||||
|
path := fmt.Sprintf("/api/v1/projects/%d/alias.json", projID)
|
||||||
|
jsonData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dataBytes, err := a.getURLContents(path, nil, http.MethodPost, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(dataBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) DestroyProject(data map[string]interface{}, projID int) (map[string]interface{}, error) {
|
||||||
|
path := fmt.Sprintf("/api/v1/projects/%d.json", projID)
|
||||||
|
jsonData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dataBytes, err := a.getURLContents(path, nil, http.MethodDelete, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(dataBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) AddProjectToRepo(data map[string]interface{}, repoID int) (map[string]interface{}, error) {
|
||||||
|
path := fmt.Sprintf("/api/v1/repositories/%d/add_project.json", repoID)
|
||||||
|
jsonData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dataBytes, err := a.getURLContents(path, nil, http.MethodPut, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(dataBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) NewProject(data map[string]interface{}) (map[string]interface{}, error) {
|
||||||
|
jsonData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dataBytes, err := a.getURLContents("/api/v1/projects.json", nil, http.MethodPost, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(dataBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) GetGitRefsList(projID int) (map[string]interface{}, error) {
|
||||||
|
path := fmt.Sprintf("/api/v1/projects/%d/refs_list.json", projID)
|
||||||
|
data, err := a.getURLContents(path, nil, http.MethodGet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) GetUserByID(userID int) (map[string]interface{}, error) {
|
||||||
|
path := fmt.Sprintf("/api/v1/users/%d.json", userID)
|
||||||
|
data, err := a.getURLContents(path, nil, http.MethodGet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) GetGroupByID(groupID int) (map[string]interface{}, error) {
|
||||||
|
path := fmt.Sprintf("/api/v1/groups/%d.json", groupID)
|
||||||
|
data, err := a.getURLContents(path, nil, http.MethodGet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) GetSearchResults(searchType, query string) (map[string]interface{}, error) {
|
||||||
|
params := url.Values{"type": {searchType}, "query": {query}, "per_page": {"100"}}
|
||||||
|
data, err := a.getURLContents("/api/v1/search.json", params, http.MethodGet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) GetList(listType string, page int) (map[string]interface{}, error) {
|
||||||
|
params := url.Values{"page": {fmt.Sprintf("%d", page)}, "per_page": {"100"}}
|
||||||
|
path := fmt.Sprintf("/api/v1/%s.json", listType)
|
||||||
|
data, err := a.getURLContents(path, params, http.MethodGet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) GetProjectsSingle(repoID, page int) (map[string]interface{}, error) {
|
||||||
|
params := url.Values{"page": {fmt.Sprintf("%d", page)}, "per_page": {"100"}}
|
||||||
|
path := fmt.Sprintf("/api/v1/repositories/%d/projects.json", repoID)
|
||||||
|
data, err := a.getURLContents(path, params, http.MethodGet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.processResponse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) ComputeSHA1(filePath string) (string, error) {
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
hasher := sha1.New()
|
||||||
|
if _, err := io.Copy(hasher, file); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%x", hasher.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) UploadFile(filePath string, silent bool) (string, error) {
|
||||||
|
hash, err := a.ComputeSHA1(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.log.Printf("File hash is %s", hash)
|
||||||
|
|
||||||
|
res, err := a.GetFileInfoByHash(hash)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res) > 0 && res[0]["sha1_hash"] == hash {
|
||||||
|
newFn := filepath.Base(filePath)
|
||||||
|
oldFn := res[0]["file_name"].(string)
|
||||||
|
if oldFn != newFn && !silent {
|
||||||
|
a.log.Printf("The name of the file in file-store is %s, but you are trying to upload file %s", oldFn, newFn)
|
||||||
|
}
|
||||||
|
return hash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
|
||||||
|
part, err := writer.CreateFormFile("file_store[file]", filePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(part, file); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
size := body.Len()
|
||||||
|
contentType := writer.FormDataContentType()
|
||||||
|
|
||||||
|
if !silent {
|
||||||
|
a.log.Printf("Uploading %s (%s)", filePath, a.bytesToHuman(int64(size), "%d%s", "basic"))
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, a.fileStoreURL+"/api/v1/upload", body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", contentType)
|
||||||
|
req.Header.Set("Content-Length", fmt.Sprintf("%d", size))
|
||||||
|
req.Header.Set("Authorization", "Basic "+a.base64AuthStr)
|
||||||
|
|
||||||
|
resp, err := a.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||||
|
return "", fmt.Errorf("could not upload file. HTTP error %d %s", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
var output map[string]interface{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&output); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
shaHash, ok := output["sha1_hash"].(string)
|
||||||
|
if !ok {
|
||||||
|
return "", errors.New("invalid response from server")
|
||||||
|
}
|
||||||
|
|
||||||
|
return shaHash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) GetFileInfoByHash(shaHash string) ([]map[string]interface{}, error) {
|
||||||
|
params := url.Values{"hash": {shaHash}}
|
||||||
|
data, err := a.getURLContents("/api/v1/file_stores.json", params, http.MethodGet, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var response []map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data, &response); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AbfJson) FetchFile(shaHash, path string) error {
|
||||||
|
URL := a.fileStoreURL + "/api/v1/file_stores/" + shaHash
|
||||||
|
resp, err := a.client.Get(URL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("failed to fetch file: HTTP error %d %s", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(out, resp.Body)
|
||||||
|
return err
|
||||||
|
}
|
252
abfapi_test.go
Normal file
252
abfapi_test.go
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
package abfapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Создаем мок-сервер для тестирования
|
||||||
|
func setupMockServer(t *testing.T) (*httptest.Server, *AbfJson) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/v1/arches.json":
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"architectures": ["x86_64", "arm64"]}`))
|
||||||
|
case "/api/v1/platforms.json":
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"platforms": [{"id": 1, "name": "Ubuntu"}, {"id": 2, "name": "CentOS"}]}`))
|
||||||
|
case "/api/v1/platforms/1.json":
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"platform": {"id": 1, "name": "Ubuntu"}}`))
|
||||||
|
case "/api/v1/users/testuser.json":
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"user": {"id": 1, "username": "testuser"}}`))
|
||||||
|
case "/api/v1/build_lists.json":
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"build_lists": [{"id": 1, "name": "BuildList1"}]}`))
|
||||||
|
} else if r.Method == http.MethodPost {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"build_list": {"id": 2, "name": "NewBuildList"}}`))
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
case "/api/v1/build_lists/1.json":
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"build_list": {"id": 1, "name": "BuildList1"}}`))
|
||||||
|
case "/api/v1/build_lists/1/publish.json":
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"status": "published"}`))
|
||||||
|
case "/api/v1/projects/1.json":
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"project": {"id": 1, "name": "TestProject"}}`))
|
||||||
|
case "/api/v1/projects/get_id.json":
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"project": {"id": 1, "name": "TestProject"}}`))
|
||||||
|
case "/api/v1/projects.json":
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"projects": [{"id": 1, "name": "TestProject"}]}`))
|
||||||
|
case "/api/v1/file_stores.json":
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`[{"sha1_hash": "1234567890abcdef1234567890abcdef12345678", "file_name": "testfile.txt"}]`))
|
||||||
|
case "/api/v1/upload":
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte(`{"sha1_hash": "1234567890abcdef1234567890abcdef12345678"}`))
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
case "/api/v1/file_stores/1234567890abcdef1234567890abcdef12345678":
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write([]byte("Hello, world!"))
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
logger := log.New(ioutil.Discard, "", log.LstdFlags)
|
||||||
|
client, err := NewAbfJson(server.URL, server.URL, "testuser", "testpass", logger)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
return server, client
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAbfJson(t *testing.T) {
|
||||||
|
logger := log.New(ioutil.Discard, "", log.LstdFlags)
|
||||||
|
client, err := NewAbfJson("http://example.com", "http://filestore.example.com", "testuser", "testpass", logger)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, client)
|
||||||
|
assert.Equal(t, "http://example.com", client.abfURL)
|
||||||
|
assert.Equal(t, "http://filestore.example.com", client.fileStoreURL)
|
||||||
|
assert.Equal(t, "dGVzdHVzZXI6dGVzdHBhc3M=", client.base64AuthStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetArchitectures(t *testing.T) {
|
||||||
|
server, client := setupMockServer(t)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
arches, err := client.GetArchitectures()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, arches)
|
||||||
|
assert.Contains(t, arches, "architectures")
|
||||||
|
assert.IsType(t, []interface{}{}, arches["architectures"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPlatforms(t *testing.T) {
|
||||||
|
server, client := setupMockServer(t)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
platforms, err := client.GetPlatforms("")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, platforms)
|
||||||
|
assert.Contains(t, platforms, "platforms")
|
||||||
|
assert.IsType(t, []interface{}{}, platforms["platforms"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPlatformByID(t *testing.T) {
|
||||||
|
server, client := setupMockServer(t)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
platform, err := client.GetPlatformByID(1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, platform)
|
||||||
|
assert.Contains(t, platform, "platform")
|
||||||
|
assert.IsType(t, map[string]interface{}{}, platform["platform"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetUserID(t *testing.T) {
|
||||||
|
server, client := setupMockServer(t)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
user, err := client.GetUserID("testuser")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, user)
|
||||||
|
assert.Contains(t, user, "user")
|
||||||
|
assert.IsType(t, map[string]interface{}{}, user["user"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetBuildListByID(t *testing.T) {
|
||||||
|
server, client := setupMockServer(t)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
buildList, err := client.GetBuildListByID(1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, buildList)
|
||||||
|
assert.Contains(t, buildList, "build_list")
|
||||||
|
assert.IsType(t, map[string]interface{}{}, buildList["build_list"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublish(t *testing.T) {
|
||||||
|
server, client := setupMockServer(t)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
result, err := client.Publish(1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, result)
|
||||||
|
assert.Contains(t, result, "status")
|
||||||
|
assert.Equal(t, "published", result["status"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetProjectByID(t *testing.T) {
|
||||||
|
server, client := setupMockServer(t)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
project, err := client.GetProjectByID(1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, project)
|
||||||
|
assert.Contains(t, project, "project")
|
||||||
|
assert.IsType(t, map[string]interface{}{}, project["project"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetProjectIDByName(t *testing.T) {
|
||||||
|
server, client := setupMockServer(t)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
project, err := client.GetProjectIDByName([2]string{"owner", "TestProject"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, project)
|
||||||
|
assert.Contains(t, project, "project")
|
||||||
|
assert.IsType(t, map[string]interface{}{}, project["project"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewProject(t *testing.T) {
|
||||||
|
server, client := setupMockServer(t)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"name": "NewProject",
|
||||||
|
"description": "This is a new project",
|
||||||
|
}
|
||||||
|
|
||||||
|
project, err := client.NewProject(data)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, project)
|
||||||
|
assert.Contains(t, project, "projects")
|
||||||
|
assert.IsType(t, []interface{}{}, project["projects"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUploadFile(t *testing.T) {
|
||||||
|
server, client := setupMockServer(t)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// Создаем временный файл для загрузки
|
||||||
|
tmpfile, err := ioutil.TempFile("", "testfile")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpfile.Name())
|
||||||
|
|
||||||
|
// Записываем данные в файл
|
||||||
|
_, err = tmpfile.Write([]byte("Hello, world!"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := tmpfile.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := client.UploadFile(tmpfile.Name(), true)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "1234567890abcdef1234567890abcdef12345678", hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetFileInfoByHash(t *testing.T) {
|
||||||
|
server, client := setupMockServer(t)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
info, err := client.GetFileInfoByHash("1234567890abcdef1234567890abcdef12345678")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, info)
|
||||||
|
assert.Len(t, info, 1)
|
||||||
|
assert.Contains(t, info[0], "sha1_hash")
|
||||||
|
assert.Equal(t, "1234567890abcdef1234567890abcdef12345678", info[0]["sha1_hash"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchFile(t *testing.T) {
|
||||||
|
server, client := setupMockServer(t)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// Создаем временный файл для сохранения загруженных данных
|
||||||
|
tmpfile, err := ioutil.TempFile("", "downloadedfile")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpfile.Name())
|
||||||
|
defer tmpfile.Close()
|
||||||
|
|
||||||
|
err = client.FetchFile("1234567890abcdef1234567890abcdef12345678", tmpfile.Name())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Проверяем содержимое файла
|
||||||
|
content, err := ioutil.ReadFile(tmpfile.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
assert.Equal(t, "Hello, world!", string(content))
|
||||||
|
}
|
16
go.mod
Normal file
16
go.mod
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
module abfapi
|
||||||
|
|
||||||
|
go 1.22.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79
|
||||||
|
github.com/stretchr/testify v1.8.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/google/btree v1.1.3 // indirect
|
||||||
|
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
23
go.sum
Normal file
23
go.sum
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||||
|
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||||
|
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
|
||||||
|
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||||
|
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
|
||||||
|
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
Loading…
Add table
Reference in a new issue