1
0
mirror of https://github.com/gilbertchen/duplicacy synced 2025-12-06 00:03:38 +00:00
Files
duplicacy/src/duplicacy_acdclient.go
2017-11-07 12:05:39 -05:00

455 lines
10 KiB
Go

// Copyright (c) Acrosync LLC. All rights reserved.
// Free for personal use and commercial trial
// Commercial use requires per-user licenses available from https://duplicacy.com
package duplicacy
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"math/rand"
"mime/multipart"
"net/http"
"sync"
"time"
"golang.org/x/oauth2"
)
type ACDError struct {
Status int
Message string `json:"message"`
}
func (err ACDError) Error() string {
return fmt.Sprintf("%d %s", err.Status, err.Message)
}
var ACDRefreshTokenURL = "https://duplicacy.com/acd_refresh"
type ACDClient struct {
HTTPClient *http.Client
TokenFile string
Token *oauth2.Token
TokenLock *sync.Mutex
ContentURL string
MetadataURL string
TestMode bool
}
func NewACDClient(tokenFile string) (*ACDClient, error) {
description, err := ioutil.ReadFile(tokenFile)
if err != nil {
return nil, err
}
token := new(oauth2.Token)
if err := json.Unmarshal(description, token); err != nil {
return nil, err
}
client := &ACDClient{
HTTPClient: http.DefaultClient,
TokenFile: tokenFile,
Token: token,
TokenLock: &sync.Mutex{},
}
client.GetEndpoint()
return client, nil
}
func (client *ACDClient) call(url string, method string, input interface{}, contentType string) (io.ReadCloser, int64, error) {
//LOG_DEBUG("ACD_CALL", "%s %s", method, url)
var response *http.Response
backoff := 1
for i := 0; i < 8; i++ {
var inputReader io.Reader
switch input.(type) {
default:
jsonInput, err := json.Marshal(input)
if err != nil {
return nil, 0, err
}
inputReader = bytes.NewReader(jsonInput)
case []byte:
inputReader = bytes.NewReader(input.([]byte))
case int:
inputReader = bytes.NewReader([]byte(""))
case *bytes.Buffer:
inputReader = bytes.NewReader(input.(*bytes.Buffer).Bytes())
case *RateLimitedReader:
input.(*RateLimitedReader).Reset()
inputReader = input.(*RateLimitedReader)
}
request, err := http.NewRequest(method, url, inputReader)
if err != nil {
return nil, 0, err
}
if reader, ok := inputReader.(*RateLimitedReader); ok {
request.ContentLength = reader.Length()
}
if url != ACDRefreshTokenURL {
client.TokenLock.Lock()
request.Header.Set("Authorization", "Bearer "+client.Token.AccessToken)
client.TokenLock.Unlock()
}
if contentType != "" {
request.Header.Set("Content-Type", contentType)
}
response, err = client.HTTPClient.Do(request)
if err != nil {
return nil, 0, err
}
if response.StatusCode < 400 {
return response.Body, response.ContentLength, nil
}
if response.StatusCode == 404 {
buffer := new(bytes.Buffer)
buffer.ReadFrom(response.Body)
response.Body.Close()
return nil, 0, ACDError{Status: response.StatusCode, Message: buffer.String()}
}
if response.StatusCode == 400 {
defer response.Body.Close()
e := &ACDError{
Status: response.StatusCode,
}
if err := json.NewDecoder(response.Body).Decode(e); err == nil {
return nil, 0, e
} else {
return nil, 0, ACDError{Status: response.StatusCode, Message: "Bad input parameter"}
}
}
response.Body.Close()
if response.StatusCode == 401 {
if url == ACDRefreshTokenURL {
return nil, 0, ACDError{Status: response.StatusCode, Message: "Unauthorized"}
}
err = client.RefreshToken()
if err != nil {
return nil, 0, err
}
continue
} else if response.StatusCode == 403 {
return nil, 0, ACDError{Status: response.StatusCode, Message: "Forbidden"}
} else if response.StatusCode == 404 {
return nil, 0, ACDError{Status: response.StatusCode, Message: "Resource not found"}
} else if response.StatusCode == 409 {
return nil, 0, ACDError{Status: response.StatusCode, Message: "Conflict"}
} else if response.StatusCode == 411 {
return nil, 0, ACDError{Status: response.StatusCode, Message: "Length required"}
} else if response.StatusCode == 412 {
return nil, 0, ACDError{Status: response.StatusCode, Message: "Precondition failed"}
} else if response.StatusCode == 429 || response.StatusCode == 500 {
reason := "Too many requests"
if response.StatusCode == 500 {
reason = "Internal server error"
}
retryAfter := time.Duration(rand.Float32() * 1000.0 * float32(backoff))
LOG_INFO("ACD_RETRY", "%s; retry after %d milliseconds", reason, retryAfter)
time.Sleep(retryAfter * time.Millisecond)
backoff *= 2
continue
} else if response.StatusCode == 503 {
return nil, 0, ACDError{Status: response.StatusCode, Message: "Service unavailable"}
} else {
return nil, 0, ACDError{Status: response.StatusCode, Message: "Unknown error"}
}
}
return nil, 0, fmt.Errorf("Maximum number of retries reached")
}
func (client *ACDClient) RefreshToken() (err error) {
client.TokenLock.Lock()
defer client.TokenLock.Unlock()
readCloser, _, err := client.call(ACDRefreshTokenURL, "POST", client.Token, "")
if err != nil {
return err
}
defer readCloser.Close()
if err = json.NewDecoder(readCloser).Decode(client.Token); err != nil {
return err
}
description, err := json.Marshal(client.Token)
if err != nil {
return err
}
err = ioutil.WriteFile(client.TokenFile, description, 0644)
if err != nil {
return err
}
return nil
}
type ACDGetEndpointOutput struct {
CustomerExists bool `json:"customerExists"`
ContentURL string `json:"contentUrl"`
MetadataURL string `json:"metadataUrl"`
}
func (client *ACDClient) GetEndpoint() (err error) {
readCloser, _, err := client.call("https://drive.amazonaws.com/drive/v1/account/endpoint", "GET", 0, "")
if err != nil {
return err
}
defer readCloser.Close()
output := &ACDGetEndpointOutput{}
if err = json.NewDecoder(readCloser).Decode(&output); err != nil {
return err
}
client.ContentURL = output.ContentURL
client.MetadataURL = output.MetadataURL
return nil
}
type ACDEntry struct {
Name string `json:"name"`
ID string `json:"id"`
Size int64 `json:"size"`
Kind string `json:"kind"`
}
type ACDListEntriesOutput struct {
Count int `json:"count"`
NextToken string `json:"nextToken"`
Entries []ACDEntry `json:"data"`
}
func (client *ACDClient) ListEntries(parentID string, listFiles bool, listDirectories bool) ([]ACDEntry, error) {
startToken := ""
entries := []ACDEntry{}
for {
url := client.MetadataURL + "nodes/" + parentID + "/children?"
if listFiles && !listDirectories {
url += "filters=kind:FILE&"
} else if !listFiles && listDirectories {
url += "filters=kind:FOLDER&"
}
if startToken != "" {
url += "startToken=" + startToken + "&"
}
if client.TestMode {
url += "limit=8"
} else {
url += "limit=200"
}
readCloser, _, err := client.call(url, "GET", 0, "")
if err != nil {
return nil, err
}
defer readCloser.Close()
output := &ACDListEntriesOutput{}
if err = json.NewDecoder(readCloser).Decode(&output); err != nil {
return nil, err
}
entries = append(entries, output.Entries...)
startToken = output.NextToken
if startToken == "" {
break
}
}
return entries, nil
}
func (client *ACDClient) ListByName(parentID string, name string) (string, bool, int64, error) {
url := client.MetadataURL + "nodes"
if parentID == "" {
url += "?filters=Kind:FOLDER+AND+isRoot:true"
} else {
url += "/" + parentID + "/children?filters=name:" + name
}
readCloser, _, err := client.call(url, "GET", 0, "")
if err != nil {
return "", false, 0, err
}
defer readCloser.Close()
output := &ACDListEntriesOutput{}
if err = json.NewDecoder(readCloser).Decode(&output); err != nil {
return "", false, 0, err
}
if len(output.Entries) == 0 {
return "", false, 0, nil
}
return output.Entries[0].ID, output.Entries[0].Kind == "FOLDER", output.Entries[0].Size, nil
}
func (client *ACDClient) DownloadFile(fileID string) (io.ReadCloser, int64, error) {
url := client.ContentURL + "nodes/" + fileID + "/content"
return client.call(url, "GET", 0, "")
}
func (client *ACDClient) UploadFile(parentID string, name string, content []byte, rateLimit int) (fileID string, err error) {
url := client.ContentURL + "nodes?suppress=deduplication"
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
metadata := make(map[string]interface{})
metadata["name"] = name
metadata["kind"] = "FILE"
metadata["parents"] = []string{parentID}
metadataJSON, err := json.Marshal(metadata)
if err != nil {
return "", err
}
err = writer.WriteField("metadata", string(metadataJSON))
if err != nil {
return "", err
}
part, err := writer.CreateFormFile("content", name)
if err != nil {
return "", err
}
_, err = part.Write(content)
if err != nil {
return "", err
}
writer.Close()
var input interface{}
input = body
if rateLimit > 0 {
input = CreateRateLimitedReader(body.Bytes(), rateLimit)
}
readCloser, _, err := client.call(url, "POST", input, writer.FormDataContentType())
if err != nil {
return "", err
}
defer readCloser.Close()
entry := ACDEntry{}
if err = json.NewDecoder(readCloser).Decode(&entry); err != nil {
return "", err
}
return entry.ID, nil
}
func (client *ACDClient) DeleteFile(fileID string) error {
url := client.MetadataURL + "trash/" + fileID
readCloser, _, err := client.call(url, "PUT", 0, "")
if err != nil {
return err
}
readCloser.Close()
return nil
}
func (client *ACDClient) MoveFile(fileID string, fromParentID string, toParentID string) error {
url := client.MetadataURL + "nodes/" + toParentID + "/children"
parameters := make(map[string]string)
parameters["fromParent"] = fromParentID
parameters["childId"] = fileID
readCloser, _, err := client.call(url, "POST", parameters, "")
if err != nil {
return err
}
readCloser.Close()
return nil
}
func (client *ACDClient) CreateDirectory(parentID string, name string) (string, error) {
url := client.MetadataURL + "nodes"
parameters := make(map[string]interface{})
parameters["name"] = name
parameters["kind"] = "FOLDER"
parameters["parents"] = []string{parentID}
readCloser, _, err := client.call(url, "POST", parameters, "")
if err != nil {
return "", err
}
defer readCloser.Close()
entry := ACDEntry{}
if err = json.NewDecoder(readCloser).Decode(&entry); err != nil {
return "", err
}
return entry.ID, nil
}