mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-21 00:24:12 +08:00
404 lines
12 KiB
Go
404 lines
12 KiB
Go
package client
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
|
)
|
|
|
|
const oneDriveGlobalBaseURL = "https://graph.microsoft.com/v1.0/"
|
|
const oneDriveChinaBaseURL = "https://microsoftgraph.chinacloudapi.cn/v1.0/"
|
|
|
|
type oneDriveClient struct {
|
|
client *http.Client
|
|
baseURL *url.URL
|
|
token string
|
|
}
|
|
|
|
func NewOneDriveClient(vars map[string]interface{}) (*oneDriveClient, error) {
|
|
token, err := RefreshToken("refresh_token", "accessToken", vars)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
baseURL := oneDriveGlobalBaseURL
|
|
if loadParamFromVars("isCN", vars) == "true" {
|
|
baseURL = oneDriveChinaBaseURL
|
|
}
|
|
parsedBaseURL, err := url.Parse(baseURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse OneDrive base URL failed: %w", err)
|
|
}
|
|
return &oneDriveClient{client: http.DefaultClient, baseURL: parsedBaseURL, token: token}, nil
|
|
}
|
|
|
|
func (o oneDriveClient) ListBuckets() ([]interface{}, error) { return nil, nil }
|
|
|
|
func (o oneDriveClient) Exist(itemPath string) (bool, error) {
|
|
_, err := o.loadIDByPath(normalizeDrivePath(itemPath))
|
|
if isOneDriveNotFound(err) {
|
|
return false, nil
|
|
}
|
|
return err == nil, err
|
|
}
|
|
|
|
func (o oneDriveClient) Size(itemPath string) (int64, error) {
|
|
var item DriveItem
|
|
if err := o.getDriveItem(context.Background(), normalizeDrivePath(itemPath), &item); err != nil {
|
|
return 0, err
|
|
}
|
|
return item.Size, nil
|
|
}
|
|
|
|
func (o oneDriveClient) Delete(itemPath string) (bool, error) {
|
|
itemPath = normalizeDrivePath(itemPath)
|
|
if err := o.doJSON(context.Background(), http.MethodDelete, "me/drive/root:"+escapeDrivePath(itemPath), nil, nil); err != nil {
|
|
return false, fmt.Errorf("delete OneDrive file failed: %w", err)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (o oneDriveClient) Upload(ctx context.Context, src, target string) (bool, error) {
|
|
target = normalizeDrivePath(target)
|
|
parentPath := path.Dir(target)
|
|
if _, err := o.loadIDByPath(parentPath); err != nil {
|
|
if !isOneDriveNotFound(err) {
|
|
return false, err
|
|
}
|
|
if err := o.createFolder(parentPath); err != nil {
|
|
return false, fmt.Errorf("create directory before upload failed: %w", err)
|
|
}
|
|
}
|
|
folderID, err := o.loadIDByPath(parentPath)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
fileInfo, err := os.Stat(src)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if fileInfo.IsDir() {
|
|
return false, errors.New("only file is allowed to be uploaded here")
|
|
}
|
|
if fileInfo.Size() < 4*1024*1024 {
|
|
return o.upSmall(ctx, src, folderID)
|
|
}
|
|
return o.upBig(ctx, src, folderID, fileInfo.Size())
|
|
}
|
|
|
|
func (o oneDriveClient) Download(src, target string) (bool, error) {
|
|
var item DriveItem
|
|
if err := o.getDriveItem(context.Background(), normalizeDrivePath(src), &item); err != nil {
|
|
return false, err
|
|
}
|
|
if item.DownloadURL == "" {
|
|
return false, errors.New("OneDrive download URL is missing")
|
|
}
|
|
resp, err := o.client.Get(item.DownloadURL)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
|
return false, newOneDriveHTTPError(resp)
|
|
}
|
|
out, err := os.Create(target)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer out.Close()
|
|
_, err = io.CopyBuffer(out, resp.Body, make([]byte, 2*1024*1024))
|
|
return err == nil, err
|
|
}
|
|
|
|
func (o *oneDriveClient) ListObjects(prefix string) ([]string, error) {
|
|
folderID, err := o.loadIDByPath(normalizeDrivePath(prefix))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var items oneDriveItemsResponse
|
|
endpoint := fmt.Sprintf("me/drive/items/%s/children", url.PathEscape(folderID))
|
|
if err := o.doJSON(context.Background(), http.MethodGet, endpoint, nil, &items); err != nil {
|
|
return nil, fmt.Errorf("list OneDrive files failed: %w", err)
|
|
}
|
|
result := make([]string, 0, len(items.Value))
|
|
for _, item := range items.Value {
|
|
result = append(result, item.Name)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (o *oneDriveClient) loadIDByPath(itemPath string) (string, error) {
|
|
var item DriveItem
|
|
if err := o.getDriveItem(context.Background(), itemPath, &item); err != nil {
|
|
return "", err
|
|
}
|
|
return item.ID, nil
|
|
}
|
|
|
|
func (o *oneDriveClient) getDriveItem(ctx context.Context, itemPath string, result *DriveItem) error {
|
|
endpoint := "me/drive/root"
|
|
if itemPath != "/" {
|
|
endpoint += ":" + escapeDrivePath(itemPath)
|
|
}
|
|
if err := o.doJSON(ctx, http.MethodGet, endpoint, nil, result); err != nil {
|
|
return fmt.Errorf("get OneDrive item failed: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (o *oneDriveClient) createFolder(parent string) error {
|
|
if parent == "/" {
|
|
return nil
|
|
}
|
|
parentID, err := o.loadIDByPath(path.Dir(parent))
|
|
if err != nil {
|
|
if !isOneDriveNotFound(err) {
|
|
return err
|
|
}
|
|
if err := o.createFolder(path.Dir(parent)); err != nil {
|
|
return err
|
|
}
|
|
parentID, err = o.loadIDByPath(path.Dir(parent))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
body := struct {
|
|
Name string `json:"name"`
|
|
Folder map[string]interface{} `json:"folder"`
|
|
}{Name: path.Base(parent), Folder: map[string]interface{}{}}
|
|
endpoint := fmt.Sprintf("me/drive/items/%s/children", url.PathEscape(parentID))
|
|
return o.doJSON(context.Background(), http.MethodPost, endpoint, body, nil)
|
|
}
|
|
|
|
func (o *oneDriveClient) upSmall(ctx context.Context, srcPath, folderID string) (bool, error) {
|
|
file, err := os.Open(srcPath)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer file.Close()
|
|
endpoint := fmt.Sprintf("me/drive/items/%s:/%s:/content?@microsoft.graph.conflictBehavior=rename", url.PathEscape(folderID), url.PathEscape(path.Base(srcPath)))
|
|
req, err := o.newGraphRequest(ctx, http.MethodPut, endpoint, file)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
req.Header.Set("Content-Type", files.GetMimeType(srcPath))
|
|
if err := o.do(req, nil); err != nil {
|
|
return false, fmt.Errorf("upload OneDrive file failed: %w", err)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (o *oneDriveClient) upBig(ctx context.Context, srcPath, folderID string, fileSize int64) (bool, error) {
|
|
file, err := os.Open(srcPath)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer file.Close()
|
|
body := struct {
|
|
Item struct {
|
|
ConflictBehavior string `json:"@microsoft.graph.conflictBehavior"`
|
|
} `json:"item"`
|
|
}{}
|
|
body.Item.ConflictBehavior = "rename"
|
|
var session oneDriveUploadSession
|
|
endpoint := fmt.Sprintf("me/drive/items/%s:/%s:/createUploadSession", url.PathEscape(folderID), url.PathEscape(path.Base(srcPath)))
|
|
if err := o.doJSON(ctx, http.MethodPost, endpoint, body, &session); err != nil {
|
|
return false, fmt.Errorf("create OneDrive upload session failed: %w", err)
|
|
}
|
|
|
|
const chunkSize int64 = 5 * 1024 * 1024
|
|
reader := bufio.NewReader(file)
|
|
buffer := make([]byte, chunkSize)
|
|
for offset := int64(0); offset < fileSize; {
|
|
length, readErr := io.ReadFull(reader, buffer)
|
|
if readErr != nil && !errors.Is(readErr, io.ErrUnexpectedEOF) && !errors.Is(readErr, io.EOF) {
|
|
return false, readErr
|
|
}
|
|
if length == 0 {
|
|
return false, io.ErrUnexpectedEOF
|
|
}
|
|
if err := o.uploadChunk(ctx, session.UploadURL, offset, fileSize, buffer[:length]); err != nil {
|
|
return false, err
|
|
}
|
|
offset += int64(length)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (o *oneDriveClient) uploadChunk(ctx context.Context, uploadURL string, offset, total int64, chunk []byte) error {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadURL, bytes.NewReader(chunk))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Content-Length", strconv.Itoa(len(chunk)))
|
|
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", offset, offset+int64(len(chunk))-1, total))
|
|
resp, err := o.client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusAccepted {
|
|
return newOneDriveHTTPError(resp)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (o *oneDriveClient) doJSON(ctx context.Context, method, endpoint string, body, result interface{}) error {
|
|
var reader io.Reader
|
|
if body != nil {
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reader = bytes.NewReader(data)
|
|
}
|
|
req, err := o.newGraphRequest(ctx, method, endpoint, reader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
return o.do(req, result)
|
|
}
|
|
|
|
func (o *oneDriveClient) newGraphRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Request, error) {
|
|
apiURL, err := o.baseURL.Parse(endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, method, apiURL.String(), body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+o.token)
|
|
return req, nil
|
|
}
|
|
|
|
func (o *oneDriveClient) do(req *http.Request, result interface{}) error {
|
|
resp, err := o.client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
|
return newOneDriveHTTPError(resp)
|
|
}
|
|
if result == nil || resp.StatusCode == http.StatusNoContent {
|
|
return nil
|
|
}
|
|
return json.NewDecoder(resp.Body).Decode(result)
|
|
}
|
|
|
|
type DriveItem struct {
|
|
Name string `json:"name"`
|
|
ID string `json:"id"`
|
|
DownloadURL string `json:"@microsoft.graph.downloadUrl"`
|
|
Size int64 `json:"size"`
|
|
}
|
|
|
|
type oneDriveItemsResponse struct {
|
|
Value []DriveItem `json:"value"`
|
|
}
|
|
|
|
type oneDriveUploadSession struct {
|
|
UploadURL string `json:"uploadUrl"`
|
|
}
|
|
|
|
type oneDriveError struct {
|
|
Details struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
} `json:"error"`
|
|
statusCode int
|
|
}
|
|
|
|
func (e *oneDriveError) Error() string {
|
|
if e.Details.Code != "" || e.Details.Message != "" {
|
|
return fmt.Sprintf("OneDrive API error: %s: %s", e.Details.Code, e.Details.Message)
|
|
}
|
|
return fmt.Sprintf("OneDrive API returned HTTP %d", e.statusCode)
|
|
}
|
|
|
|
func newOneDriveHTTPError(resp *http.Response) error {
|
|
apiErr := &oneDriveError{statusCode: resp.StatusCode}
|
|
if err := json.NewDecoder(resp.Body).Decode(apiErr); err != nil {
|
|
return fmt.Errorf("OneDrive API returned HTTP %d", resp.StatusCode)
|
|
}
|
|
return apiErr
|
|
}
|
|
|
|
func isOneDriveNotFound(err error) bool {
|
|
var apiErr *oneDriveError
|
|
return errors.As(err, &apiErr) && (apiErr.statusCode == http.StatusNotFound || apiErr.Details.Code == "itemNotFound")
|
|
}
|
|
|
|
func normalizeDrivePath(itemPath string) string { return "/" + strings.TrimPrefix(itemPath, "/") }
|
|
|
|
func escapeDrivePath(itemPath string) string {
|
|
parts := strings.Split(strings.TrimPrefix(itemPath, "/"), "/")
|
|
for i, part := range parts {
|
|
parts[i] = url.PathEscape(part)
|
|
}
|
|
return "/" + strings.Join(parts, "/")
|
|
}
|
|
|
|
func RefreshToken(grantType string, tokenType string, varMap map[string]interface{}) (string, error) {
|
|
data := url.Values{}
|
|
isCN := loadParamFromVars("isCN", varMap)
|
|
data.Set("client_id", loadParamFromVars("client_id", varMap))
|
|
data.Set("client_secret", loadParamFromVars("client_secret", varMap))
|
|
if grantType == "refresh_token" {
|
|
data.Set("grant_type", "refresh_token")
|
|
data.Set("refresh_token", loadParamFromVars("refresh_token", varMap))
|
|
} else {
|
|
data.Set("grant_type", "authorization_code")
|
|
data.Set("code", loadParamFromVars("code", varMap))
|
|
}
|
|
data.Set("redirect_uri", loadParamFromVars("redirect_uri", varMap))
|
|
tokenURL := "https://login.microsoftonline.com/common/oauth2/v2.0/token"
|
|
if isCN == "true" {
|
|
tokenURL = "https://login.chinacloudapi.cn/common/oauth2/v2.0/token"
|
|
}
|
|
req, err := http.NewRequest(http.MethodPost, tokenURL, strings.NewReader(data.Encode()))
|
|
if err != nil {
|
|
return "", fmt.Errorf("create access token request failed: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("request access token failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
|
return "", newOneDriveHTTPError(resp)
|
|
}
|
|
var tokenMap map[string]interface{}
|
|
if err := json.NewDecoder(resp.Body).Decode(&tokenMap); err != nil {
|
|
return "", fmt.Errorf("decode token response failed: %w", err)
|
|
}
|
|
key := "refresh_token"
|
|
if tokenType == "accessToken" {
|
|
key = "access_token"
|
|
}
|
|
token, ok := tokenMap[key].(string)
|
|
if !ok {
|
|
return "", fmt.Errorf("no %s in token response", key)
|
|
}
|
|
return token, nil
|
|
}
|