package yadisk import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "os" "strconv" "strings" "time" "golang.org/x/oauth2" "penahub.gitlab.yandexcloud.net/backend/templategen/tools" ) const ( BaseURL = "https://cloud-api.yandex.net" OauthURL = "https://oauth.yandex.ru" V1DiskAPI = BaseURL + "/v1/disk" DefaultFolder = "disk:/templategen" DefaultTemplateFolder = DefaultFolder + "/templates" DefaultSaveFolder = DefaultFolder + "/saved" TokenTypeOAuth = "OAuth" ) 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: OauthURL + "/authorize", TokenURL: OauthURL + "/token", }, RedirectURL: redirectURI, Scopes: nil, }, ServiceURL: serviceURL, } } func (ca *ClientApp) GenerateOAuthURL(amoID, penaID, redirectURL string) (string, error) { state, err := tools.EncryptTokenAES(tools.StateToken{ AmoID: amoID, PenaID: penaID, 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 = TokenTypeOAuth 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 = TokenTypeOAuth return token, nil } func (c *Client) GetDisk(ctx context.Context) (*Disk, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, V1DiskAPI, http.NoBody) if err != nil { return nil, err } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() 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(ctx context.Context) (*User, error) { disk, err := c.GetDisk(ctx) if err != nil { return nil, err } return &disk.User, nil } type DResp struct { Href string `json:"href"` Method string `json:"method"` Templated bool `json:"templated"` } func (c *Client) DownloadFile(path string) (*DResp, error) { data := url.Values{} data.Set("path", path) req, err := http.NewRequest(http.MethodGet, V1DiskAPI+"/resources/download?"+data.Encode(), http.NoBody) if err != nil { return nil, err } res, err := c.HTTPClient.Do(req) if err != nil { return nil, err } defer res.Body.Close() if err := checkError(res); err != nil { return nil, err } var resp DResp if err := json.NewDecoder(res.Body).Decode(&resp); err != nil { return nil, err } return &resp, nil } func (c *Client) GetResources(ctx context.Context, 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.NewRequestWithContext(ctx, http.MethodGet, V1DiskAPI+"/resources?"+data.Encode(), http.NoBody) if err != nil { return nil, err } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound { 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(ctx context.Context, path, downloadPath string) error { res, err := c.GetResources(ctx, path, 0, 0) if err != nil { return err } if res == nil { return errors.New("file not found") } req, err := http.NewRequestWithContext(ctx, http.MethodGet, res.File, http.NoBody) if err != nil { return err } resp, err := http.DefaultClient.Do(req) if checkError(resp) != nil { return err } defer resp.Body.Close() // Create the file out, err := os.Create(downloadPath) if err != nil { return err } defer out.Close() //nolint // Write the body to file _, err = io.Copy(out, resp.Body) return err } func (c *Client) DownloadResourcesBytes(ctx context.Context, path string) ([]byte, error) { res, err := c.GetResources(ctx, path, 0, 0) if err != nil { return nil, err } if res == nil { return nil, errors.New("file not found") } req, err := http.NewRequestWithContext(ctx, http.MethodGet, res.File, http.NoBody) if err != nil { return nil, err } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if err = checkError(resp); err != nil { return nil, err } return io.ReadAll(resp.Body) } // PutResources - создание папки. func (c *Client) PutResources(path string) (*RespPutResources, error) { data := url.Values{} data.Set("path", path) req, err := http.NewRequest(http.MethodPut, V1DiskAPI+"/resources?"+data.Encode(), http.NoBody) if err != nil { return nil, err } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode == http.StatusConflict { 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(ctx context.Context, path string) error { data := url.Values{} data.Set("path", path) req, err := http.NewRequestWithContext(ctx, http.MethodDelete, V1DiskAPI+"/resources?"+data.Encode(), http.NoBody) if err != nil { return err } resp, err := c.HTTPClient.Do(req) if err != nil { return err } defer resp.Body.Close() err = checkError(resp) return err } // UploadResourcesURL - Загрузить файл на Яндекс диск по URL. Нерекомендуемый. func (c *Client) UploadResourcesURL(ctx context.Context, path, downloadPath string) (string, error) { 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.NewRequestWithContext(ctx, http.MethodPost, V1DiskAPI+"/resources/upload?"+data.Encode(), http.NoBody) fmt.Println("req:", req.URL) if err != nil { return "", err } resp, err := c.HTTPClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() 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(ctx, r.Href) if err != nil { return "", err } // Ожидаем пока загрузится файл на Я.Диск. Timeout: 5 sec; tick: 100 ms; timer := time.NewTimer(5 * time.Second) ticker := time.NewTicker(100 * time.Millisecond) defer ticker.Stop() for status == "in-progress" { select { case <-timer.C: status = "timeout" case <-ticker.C: status, err = c.GetOperationsByURL(ctx, r.Href) if err != nil { timer.Stop() return "", err } } } timer.Stop() if status != "success" { return "", fmt.Errorf("bad upload status: %v", status) } resource, err := c.GetResources(ctx, 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(ctx context.Context, path string, file io.Reader) (string, string, error) { // Get url for request target, err := c.getUploadResourcesURL(ctx, path) if err != nil { return "", "", err } if target == nil { return "", "", errors.New("got empty url for upload") } req, err := http.NewRequestWithContext(ctx, target.Method, target.Href, file) if err != nil { return "", "", err } resp, err := c.HTTPClient.Do(req) if err != nil { return "", "", err } defer resp.Body.Close() err = checkError(resp) if err != nil { return "", "", err } // Get uploaded data resource, err := c.GetResources(ctx, 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(ctx context.Context, path string) (*RespUploadResources, error) { data := url.Values{} data.Set("path", path) data.Set("overwrite", "true") req, err := http.NewRequestWithContext(ctx, http.MethodGet, V1DiskAPI+"/resources/upload?"+data.Encode(), http.NoBody) if err != nil { return nil, err } resp, err := c.HTTPClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() 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(ctx context.Context, operationID string) (string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, V1DiskAPI+"/operations/"+operationID, http.NoBody) if err != nil { return "", err } resp, err := c.HTTPClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() 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(ctx context.Context, url string) (string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) if err != nil { return "", err } resp, err := c.HTTPClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() err = checkError(resp) if err != nil { return "", err } r := Operation{} err = json.NewDecoder(resp.Body).Decode(&r) return r.Status, err } // PublishResource - публикует ресурс. func (c *Client) PublishResource(ctx context.Context, path string) error { data := url.Values{} data.Set("path", path) req, err := http.NewRequestWithContext(ctx, http.MethodPut, V1DiskAPI+"/resources/publish?"+data.Encode(), http.NoBody) if err != nil { return err } resp, err := c.HTTPClient.Do(req) if err != nil { return err } defer resp.Body.Close() err = checkError(resp) if err != nil { return err } return nil } // checkError - если статус не в диапазоне 200-х вернет расшифровку ошибки. func checkError(resp *http.Response) error { switch resp.StatusCode { case http.StatusOK, http.StatusCreated, http.StatusAccepted, http.StatusNonAuthoritativeInfo, http.StatusNoContent, http.StatusResetContent, http.StatusPartialContent, http.StatusMultiStatus, http.StatusAlreadyReported, http.StatusIMUsed: return nil } r := Error{} err := json.NewDecoder(resp.Body).Decode(&r) if err != nil { return err } return fmt.Errorf("api/yaDisk (%v) err: %v | %v (%v)", resp.StatusCode, r.Message, r.Description, r.Error) }