mirror of https://github.com/Xhofe/alist
feat(driver): add Gofile storage driver (#9318)
Add support for Gofile.io cloud storage service with full CRUD operations. Features: - File and folder listing - Upload and download functionality - Create, move, rename, copy, and delete operations - Direct link generation for file access - API token authentication The driver implements all required driver interfaces and follows the existing driver patterns in the codebase.pull/7097/merge
parent
d0026030cb
commit
28a8428559
|
@ -32,6 +32,7 @@ import (
|
|||
_ "github.com/alist-org/alist/v3/drivers/ftp"
|
||||
_ "github.com/alist-org/alist/v3/drivers/github"
|
||||
_ "github.com/alist-org/alist/v3/drivers/github_releases"
|
||||
_ "github.com/alist-org/alist/v3/drivers/gofile"
|
||||
_ "github.com/alist-org/alist/v3/drivers/google_drive"
|
||||
_ "github.com/alist-org/alist/v3/drivers/google_photo"
|
||||
_ "github.com/alist-org/alist/v3/drivers/halalcloud"
|
||||
|
|
|
@ -0,0 +1,261 @@
|
|||
package gofile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Gofile struct {
|
||||
model.Storage
|
||||
Addition
|
||||
|
||||
accountId string
|
||||
}
|
||||
|
||||
func (d *Gofile) Config() driver.Config {
|
||||
return config
|
||||
}
|
||||
|
||||
func (d *Gofile) GetAddition() driver.Additional {
|
||||
return &d.Addition
|
||||
}
|
||||
|
||||
func (d *Gofile) Init(ctx context.Context) error {
|
||||
if d.APIToken == "" {
|
||||
return fmt.Errorf("API token is required")
|
||||
}
|
||||
|
||||
// Get account ID
|
||||
accountId, err := d.getAccountId(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get account ID: %w", err)
|
||||
}
|
||||
d.accountId = accountId
|
||||
|
||||
// Get account info to set root folder if not specified
|
||||
if d.RootFolderID == "" {
|
||||
accountInfo, err := d.getAccountInfo(ctx, accountId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get account info: %w", err)
|
||||
}
|
||||
d.RootFolderID = accountInfo.Data.RootFolder
|
||||
}
|
||||
|
||||
// Save driver storage
|
||||
op.MustSaveDriverStorage(d)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Gofile) Drop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Gofile) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||
var folderId string
|
||||
if dir.GetID() == "" {
|
||||
folderId = d.GetRootId()
|
||||
} else {
|
||||
folderId = dir.GetID()
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("/contents/%s", folderId)
|
||||
|
||||
var response ContentsResponse
|
||||
err := d.getJSON(ctx, endpoint, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var objects []model.Obj
|
||||
|
||||
// Process children or contents
|
||||
contents := response.Data.Children
|
||||
if contents == nil {
|
||||
contents = response.Data.Contents
|
||||
}
|
||||
|
||||
for _, content := range contents {
|
||||
objects = append(objects, d.convertContentToObj(content))
|
||||
}
|
||||
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
func (d *Gofile) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||
if file.IsDir() {
|
||||
return nil, errs.NotFile
|
||||
}
|
||||
|
||||
// Create a direct link for the file
|
||||
directLink, err := d.createDirectLink(ctx, file.GetID())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create direct link: %w", err)
|
||||
}
|
||||
|
||||
return &model.Link{
|
||||
URL: directLink,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Gofile) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||
var parentId string
|
||||
if parentDir.GetID() == "" {
|
||||
parentId = d.GetRootId()
|
||||
} else {
|
||||
parentId = parentDir.GetID()
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"parentFolderId": parentId,
|
||||
"folderName": dirName,
|
||||
}
|
||||
|
||||
var response CreateFolderResponse
|
||||
err := d.postJSON(ctx, "/contents/createFolder", data, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.Object{
|
||||
ID: response.Data.ID,
|
||||
Name: response.Data.Name,
|
||||
IsFolder: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Gofile) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
var dstId string
|
||||
if dstDir.GetID() == "" {
|
||||
dstId = d.GetRootId()
|
||||
} else {
|
||||
dstId = dstDir.GetID()
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"contentsId": srcObj.GetID(),
|
||||
"folderId": dstId,
|
||||
}
|
||||
|
||||
err := d.putJSON(ctx, "/contents/move", data, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return updated object
|
||||
return &model.Object{
|
||||
ID: srcObj.GetID(),
|
||||
Name: srcObj.GetName(),
|
||||
Size: srcObj.GetSize(),
|
||||
Modified: srcObj.ModTime(),
|
||||
IsFolder: srcObj.IsDir(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Gofile) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||
data := map[string]interface{}{
|
||||
"attribute": "name",
|
||||
"attributeValue": newName,
|
||||
}
|
||||
|
||||
var response UpdateResponse
|
||||
err := d.putJSON(ctx, fmt.Sprintf("/contents/%s/update", srcObj.GetID()), data, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.Object{
|
||||
ID: srcObj.GetID(),
|
||||
Name: newName,
|
||||
Size: srcObj.GetSize(),
|
||||
Modified: srcObj.ModTime(),
|
||||
IsFolder: srcObj.IsDir(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Gofile) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||
var dstId string
|
||||
if dstDir.GetID() == "" {
|
||||
dstId = d.GetRootId()
|
||||
} else {
|
||||
dstId = dstDir.GetID()
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"contentsId": srcObj.GetID(),
|
||||
"folderId": dstId,
|
||||
}
|
||||
|
||||
var response CopyResponse
|
||||
err := d.postJSON(ctx, "/contents/copy", data, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get the new ID from the response
|
||||
newId := srcObj.GetID()
|
||||
if response.Data.CopiedContents != nil {
|
||||
if id, ok := response.Data.CopiedContents[srcObj.GetID()]; ok {
|
||||
newId = id
|
||||
}
|
||||
}
|
||||
|
||||
return &model.Object{
|
||||
ID: newId,
|
||||
Name: srcObj.GetName(),
|
||||
Size: srcObj.GetSize(),
|
||||
Modified: srcObj.ModTime(),
|
||||
IsFolder: srcObj.IsDir(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Gofile) Remove(ctx context.Context, obj model.Obj) error {
|
||||
data := map[string]interface{}{
|
||||
"contentsId": obj.GetID(),
|
||||
}
|
||||
|
||||
return d.deleteJSON(ctx, "/contents", data)
|
||||
}
|
||||
|
||||
func (d *Gofile) Put(ctx context.Context, dstDir model.Obj, fileStreamer model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||
var folderId string
|
||||
if dstDir.GetID() == "" {
|
||||
folderId = d.GetRootId()
|
||||
} else {
|
||||
folderId = dstDir.GetID()
|
||||
}
|
||||
|
||||
response, err := d.uploadFile(ctx, folderId, fileStreamer, up)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.Object{
|
||||
ID: response.Data.FileId,
|
||||
Name: response.Data.FileName,
|
||||
Size: fileStreamer.GetSize(),
|
||||
IsFolder: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *Gofile) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *Gofile) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *Gofile) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (d *Gofile) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) {
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
var _ driver.Driver = (*Gofile)(nil)
|
|
@ -0,0 +1,26 @@
|
|||
package gofile
|
||||
|
||||
import (
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/op"
|
||||
)
|
||||
|
||||
type Addition struct {
|
||||
driver.RootID
|
||||
APIToken string `json:"api_token" required:"true" help:"Get your API token from your Gofile profile page"`
|
||||
}
|
||||
|
||||
var config = driver.Config{
|
||||
Name: "Gofile",
|
||||
DefaultRoot: "",
|
||||
LocalSort: false,
|
||||
OnlyProxy: false,
|
||||
NoCache: false,
|
||||
NoUpload: false,
|
||||
}
|
||||
|
||||
func init() {
|
||||
op.RegisterDriver(func() driver.Driver {
|
||||
return &Gofile{}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
package gofile
|
||||
|
||||
import "time"
|
||||
|
||||
type APIResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type AccountResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type AccountInfoResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Email string `json:"email"`
|
||||
RootFolder string `json:"rootFolder"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type Content struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"` // "file" or "folder"
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
CreateTime int64 `json:"createTime"`
|
||||
ModTime int64 `json:"modTime,omitempty"`
|
||||
DirectLink string `json:"directLink,omitempty"`
|
||||
Children map[string]Content `json:"children,omitempty"`
|
||||
ParentFolder string `json:"parentFolder,omitempty"`
|
||||
MD5 string `json:"md5,omitempty"`
|
||||
MimeType string `json:"mimeType,omitempty"`
|
||||
Link string `json:"link,omitempty"`
|
||||
}
|
||||
|
||||
type ContentsResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
IsOwner bool `json:"isOwner"`
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
ParentFolder string `json:"parentFolder"`
|
||||
CreateTime int64 `json:"createTime"`
|
||||
ChildrenList []string `json:"childrenList,omitempty"`
|
||||
Children map[string]Content `json:"children,omitempty"`
|
||||
Contents map[string]Content `json:"contents,omitempty"`
|
||||
Public bool `json:"public,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Tags string `json:"tags,omitempty"`
|
||||
Expiry int64 `json:"expiry,omitempty"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type UploadResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
DownloadPage string `json:"downloadPage"`
|
||||
Code string `json:"code"`
|
||||
ParentFolder string `json:"parentFolder"`
|
||||
FileId string `json:"fileId"`
|
||||
FileName string `json:"fileName"`
|
||||
GuestToken string `json:"guestToken,omitempty"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type DirectLinkResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
DirectLink string `json:"directLink"`
|
||||
ID string `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type CreateFolderResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
ParentFolder string `json:"parentFolder"`
|
||||
CreateTime int64 `json:"createTime"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type CopyResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
CopiedContents map[string]string `json:"copiedContents"` // oldId -> newId mapping
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type UpdateResponse struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Status string `json:"status"`
|
||||
Error struct {
|
||||
Message string `json:"message"`
|
||||
Code string `json:"code"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
func (c *Content) ModifiedTime() time.Time {
|
||||
if c.ModTime > 0 {
|
||||
return time.Unix(c.ModTime, 0)
|
||||
}
|
||||
return time.Unix(c.CreateTime, 0)
|
||||
}
|
||||
|
||||
func (c *Content) IsDir() bool {
|
||||
return c.Type == "folder"
|
||||
}
|
|
@ -0,0 +1,257 @@
|
|||
package gofile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/alist-org/alist/v3/drivers/base"
|
||||
"github.com/alist-org/alist/v3/internal/driver"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
)
|
||||
|
||||
const (
|
||||
baseAPI = "https://api.gofile.io"
|
||||
uploadAPI = "https://upload.gofile.io"
|
||||
)
|
||||
|
||||
func (d *Gofile) request(ctx context.Context, method, endpoint string, body io.Reader, headers map[string]string) (*http.Response, error) {
|
||||
var url string
|
||||
if strings.HasPrefix(endpoint, "http") {
|
||||
url = endpoint
|
||||
} else {
|
||||
url = baseAPI + endpoint
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+d.APIToken)
|
||||
req.Header.Set("User-Agent", "AList/3.0")
|
||||
|
||||
for k, v := range headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
return base.HttpClient.Do(req)
|
||||
}
|
||||
|
||||
func (d *Gofile) getJSON(ctx context.Context, endpoint string, result interface{}) error {
|
||||
resp, err := d.request(ctx, "GET", endpoint, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return d.handleError(resp)
|
||||
}
|
||||
|
||||
return json.NewDecoder(resp.Body).Decode(result)
|
||||
}
|
||||
|
||||
func (d *Gofile) postJSON(ctx context.Context, endpoint string, data interface{}, result interface{}) error {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
resp, err := d.request(ctx, "POST", endpoint, bytes.NewBuffer(jsonData), headers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return d.handleError(resp)
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
return json.NewDecoder(resp.Body).Decode(result)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Gofile) putJSON(ctx context.Context, endpoint string, data interface{}, result interface{}) error {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
resp, err := d.request(ctx, "PUT", endpoint, bytes.NewBuffer(jsonData), headers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return d.handleError(resp)
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
return json.NewDecoder(resp.Body).Decode(result)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Gofile) deleteJSON(ctx context.Context, endpoint string, data interface{}) error {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
resp, err := d.request(ctx, "DELETE", endpoint, bytes.NewBuffer(jsonData), headers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return d.handleError(resp)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Gofile) handleError(resp *http.Response) error {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
var errorResp ErrorResponse
|
||||
if err := json.Unmarshal(body, &errorResp); err == nil {
|
||||
return fmt.Errorf("gofile API error: %s (code: %s)", errorResp.Error.Message, errorResp.Error.Code)
|
||||
}
|
||||
|
||||
return fmt.Errorf("gofile API error: HTTP %d - %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
func (d *Gofile) uploadFile(ctx context.Context, folderId string, file model.FileStreamer, up driver.UpdateProgress) (*UploadResponse, error) {
|
||||
var body bytes.Buffer
|
||||
writer := multipart.NewWriter(&body)
|
||||
|
||||
if folderId != "" {
|
||||
writer.WriteField("folderId", folderId)
|
||||
}
|
||||
|
||||
part, err := writer.CreateFormFile("file", filepath.Base(file.GetName()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Copy with progress tracking if available
|
||||
if up != nil {
|
||||
reader := &progressReader{
|
||||
reader: file,
|
||||
total: file.GetSize(),
|
||||
up: up,
|
||||
}
|
||||
_, err = io.Copy(part, reader)
|
||||
} else {
|
||||
_, err = io.Copy(part, file)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
writer.Close()
|
||||
|
||||
headers := map[string]string{
|
||||
"Content-Type": writer.FormDataContentType(),
|
||||
}
|
||||
|
||||
resp, err := d.request(ctx, "POST", uploadAPI+"/uploadfile", &body, headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, d.handleError(resp)
|
||||
}
|
||||
|
||||
var result UploadResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
return &result, err
|
||||
}
|
||||
|
||||
func (d *Gofile) createDirectLink(ctx context.Context, contentId string) (string, error) {
|
||||
data := map[string]interface{}{}
|
||||
|
||||
var result DirectLinkResponse
|
||||
err := d.postJSON(ctx, fmt.Sprintf("/contents/%s/directlinks", contentId), data, &result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return result.Data.DirectLink, nil
|
||||
}
|
||||
|
||||
func (d *Gofile) convertContentToObj(content Content) model.Obj {
|
||||
return &model.ObjThumb{
|
||||
Object: model.Object{
|
||||
ID: content.ID,
|
||||
Name: content.Name,
|
||||
Size: content.Size,
|
||||
Modified: content.ModifiedTime(),
|
||||
IsFolder: content.IsDir(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Gofile) getAccountId(ctx context.Context) (string, error) {
|
||||
var result AccountResponse
|
||||
err := d.getJSON(ctx, "/accounts/getid", &result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result.Data.ID, nil
|
||||
}
|
||||
|
||||
func (d *Gofile) getAccountInfo(ctx context.Context, accountId string) (*AccountInfoResponse, error) {
|
||||
var result AccountInfoResponse
|
||||
err := d.getJSON(ctx, fmt.Sprintf("/accounts/%s", accountId), &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// progressReader wraps an io.Reader to track upload progress
|
||||
type progressReader struct {
|
||||
reader io.Reader
|
||||
total int64
|
||||
read int64
|
||||
up driver.UpdateProgress
|
||||
}
|
||||
|
||||
func (pr *progressReader) Read(p []byte) (n int, err error) {
|
||||
n, err = pr.reader.Read(p)
|
||||
pr.read += int64(n)
|
||||
if pr.up != nil && pr.total > 0 {
|
||||
progress := float64(pr.read) * 100 / float64(pr.total)
|
||||
pr.up(progress)
|
||||
}
|
||||
return n, err
|
||||
}
|
Loading…
Reference in New Issue