package YaDisk import ( "context" "encoding/json" "errors" "fmt" "golang.org/x/oauth2" "io" "net/http" "net/url" "os" "penahub.gitlab.yandexcloud.net/backend/templategen/tools" "strconv" "strings" "time" ) const ( BASE_URL = "https://cloud-api.yandex.net" OAUTH_URL = "https://oauth.yandex.ru" DEFAULT_FOLDER = "disk:/templategen" DEFAULT_TEMPLATE_FOLDER = DEFAULT_FOLDER + "/templates" DEFAULT_SAVE_FOLDER = DEFAULT_FOLDER + "/saved" ) type Client struct { App *ClientApp HTTPClient *http.Client Token *oauth2.Token } type ClientApp struct { Config *oauth2.Config ServiceUrl string } func NewClientApp(clientID, clientSecret, redirectUri, serviceUrl string) *ClientApp { return &ClientApp{ Config: &oauth2.Config{ ClientID: clientID, ClientSecret: clientSecret, Endpoint: oauth2.Endpoint{ AuthURL: "https://oauth.yandex.ru/authorize", TokenURL: "https://oauth.yandex.ru/token", }, RedirectURL: redirectUri, Scopes: nil, }, ServiceUrl: serviceUrl, } } func (ca *ClientApp) GenerateOAuthUrl(userId, redirectUrl string) (string, error) { state, err := tools.EncryptTokenRC4(tools.StateToken{ UserID: userId, Service: "yandex", RedirectUrl: redirectUrl, }) if err != nil { return "", err } return ca.Config.AuthCodeURL( state, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("display", "popup"), oauth2.SetAuthURLParam("force_confirm", "true"), ), nil } func (ca *ClientApp) NewClient(ctx context.Context, token *oauth2.Token, code string) (*Client, error) { var err error if code != "" { token, err = ca.Config.Exchange(ctx, code) } // Этот костыль нужен, т.к. Яндекс принимает токены только типа OAuth, хоть и отправляет типа bearer token.TokenType = "OAuth" return &Client{ App: ca, HTTPClient: ca.Config.Client(ctx, token), Token: token, }, err } func (ca *ClientApp) RefreshToken(ctx context.Context, oldToken *oauth2.Token) (*oauth2.Token, error) { token, err := ca.Config.TokenSource(ctx, oldToken).Token() if err != nil { return nil, err } // Этот костыль нужен, т.к. Яндекс принимает токены только типа OAuth, хоть и отправляет типа bearer token.TokenType = "OAuth" return token, nil } func (c *Client) GetDisk() (*Disk, error) { req, err := http.NewRequest("GET", BASE_URL+"/v1/disk", nil) if err != nil { return nil, err } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, err } err = checkError(resp) if err != nil { return nil, err } r := Disk{} err = json.NewDecoder(resp.Body).Decode(&r) if err != nil { return nil, err } return &r, nil } func (c *Client) GetUser() (*User, error) { disk, err := c.GetDisk() if err != nil { return nil, err } return &disk.User, nil } func (c *Client) GetResources(path string, limit, offset int) (*Resource, error) { if path == "" { path = "disk:/" } data := url.Values{} data.Set("path", path) if limit > 0 { data.Set("limit", strconv.Itoa(limit)) } if offset > 0 { data.Set("offset", strconv.Itoa(offset)) } data.Encode() req, err := http.NewRequest("GET", BASE_URL+"/v1/disk/resources?"+data.Encode(), nil) if err != nil { return nil, err } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, err } if resp.StatusCode == 404 { return nil, nil } err = checkError(resp) if err != nil { return nil, err } r := Resource{} err = json.NewDecoder(resp.Body).Decode(&r) return &r, err } func (c *Client) DownloadResource(path, downloadPath string) error { res, err := c.GetResources(path, 0, 0) if res == nil { return errors.New("file not found") } if err != nil { return err } resp, err := http.Get(res.File) if err != nil { return err } defer resp.Body.Close() if checkError(resp) != nil { return err } // Create the file out, err := os.Create(downloadPath) if err != nil { return err } defer out.Close() // Write the body to file _, err = io.Copy(out, resp.Body) return err } // PutResources - создание папки func (c *Client) PutResources(path string) (*RespPutResources, error) { data := url.Values{} data.Set("path", path) req, err := http.NewRequest("PUT", BASE_URL+"/v1/disk/resources?"+data.Encode(), nil) if err != nil { return nil, err } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, err } if resp.StatusCode == 409 { return nil, nil } err = checkError(resp) if err != nil { return nil, err } var r RespPutResources err = json.NewDecoder(resp.Body).Decode(&r) return &r, err } func (c *Client) DeleteResources(path string) error { data := url.Values{} data.Set("path", path) req, err := http.NewRequest("DELETE", BASE_URL+"/v1/disk/resources?"+data.Encode(), nil) if err != nil { return err } resp, err := c.HTTPClient.Do(req) if err != nil { return err } err = checkError(resp) return err } // UploadResourcesUrl - Загрузить файл на Яндекс диск по URL. Нерекомендуемый func (c *Client) UploadResourcesUrl(path, downloadPath string) (string, error) { downloadPath = strings.TrimLeft(downloadPath, "..") downloadPath = strings.TrimLeft(downloadPath, ".") fmt.Println("dp:", c.App.ServiceUrl+downloadPath) fmt.Println("path:", path) data := url.Values{} data.Set("path", path) data.Set("url", c.App.ServiceUrl+downloadPath) req, err := http.NewRequest("POST", BASE_URL+"/v1/disk/resources/upload?"+data.Encode(), nil) fmt.Println("req:", req.URL) if err != nil { return "", err } resp, err := c.HTTPClient.Do(req) if err != nil { return "", err } if checkError(resp) != nil { return "", err } var r RespUploadResources err = json.NewDecoder(resp.Body).Decode(&r) if err != nil { return "", err } status, err := c.GetOperationsByUrl(r.Href) if err != nil { return "", err } // Ожидаем пока загрузится файл на Я.Диск. Timeout: 5 sec; tick: 100 ms; timer := time.NewTimer(5 * time.Second) for status == "in-progress" { select { case <-timer.C: status = "timeout" case <-time.Tick(100 * time.Millisecond): status, err = c.GetOperationsByUrl(r.Href) if err != nil { timer.Stop() return "", err } } } timer.Stop() if status != "success" { return "", errors.New(fmt.Sprintf("bad upload status: %v", status)) } resource, err := c.GetResources(path, 0, 0) if err != nil { return "", err } var exportUrl string if resource != nil { exportUrl = resource.File } else { return "", errors.New("resource not found") } return exportUrl, err } // UploadResources - Загрузить файл на Яндекс диск func (c *Client) UploadResources(path string, file io.Reader) (string, string, error) { // Get url for request target, err := c.getUploadResourcesUrl(path) if err != nil { return "", "", err } if target == nil { return "", "", errors.New("got empty url for upload") } req, err := http.NewRequest(target.Method, target.Href, file) if err != nil { return "", "", err } resp, err := c.HTTPClient.Do(req) if err := checkError(resp); err != nil { return "", "", err } // Get uploaded data resource, err := c.GetResources(path, 0, 0) if err != nil { return "", "", err } var exportUrl string if resource != nil { exportUrl = resource.File } else { return "", "", errors.New("resource not found") } return resource.Path, exportUrl, err } // getUploadResourcesUrl - получить ссылку для отправки файла по TLS func (c *Client) getUploadResourcesUrl(path string) (*RespUploadResources, error) { data := url.Values{} data.Set("path", path) data.Set("overwrite", "true") req, err := http.NewRequest("GET", BASE_URL+"/v1/disk/resources/upload?"+data.Encode(), nil) if err != nil { return nil, err } resp, err := c.HTTPClient.Do(req) err = checkError(resp) if err != nil { return nil, err } r := RespUploadResources{} err = json.NewDecoder(resp.Body).Decode(&r) return &r, err } // GetOperations - возвращает статус асинхронной операции func (c *Client) GetOperations(operationID string) (string, error) { req, err := http.NewRequest("GET", BASE_URL+"/v1/disk/operations/"+operationID, nil) if err != nil { return "", err } resp, err := c.HTTPClient.Do(req) err = checkError(resp) if err != nil { return "", err } r := Operation{} err = json.NewDecoder(resp.Body).Decode(&r) return r.Status, err } // GetOperationsByUrl - возвращает статус асинхронной операции func (c *Client) GetOperationsByUrl(url string) (string, error) { req, err := http.NewRequest("GET", url, nil) if err != nil { return "", err } resp, err := c.HTTPClient.Do(req) err = checkError(resp) if err != nil { return "", err } r := Operation{} err = json.NewDecoder(resp.Body).Decode(&r) return r.Status, err } // checkError - если статус не в диапазоне 200-х вернет расшифровку ошибки func checkError(resp *http.Response) error { switch resp.StatusCode { case 200, 201, 202, 203, 204, 205, 206, 207, 208, 226: return nil } r := Error{} err := json.NewDecoder(resp.Body).Decode(&r) if err != nil { return err } return errors.New(fmt.Sprintf("api/yaDisk (%v) err: %v | %v (%v)", resp.StatusCode, r.Message, r.Description, r.Error)) }