docxTemplater/yadisk/api.go
2024-12-16 12:17:48 +03:00

587 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package yadisk
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"golang.org/x/oauth2"
"gitea.pena/PenaSide/docxTemplater/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)
}