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
Chesyre 2025-09-11 05:46:31 +02:00 committed by GitHub
parent d0026030cb
commit 28a8428559
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 669 additions and 0 deletions

View File

@ -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"

261
drivers/gofile/driver.go Normal file
View File

@ -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)

26
drivers/gofile/meta.go Normal file
View File

@ -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{}
})
}

124
drivers/gofile/types.go Normal file
View File

@ -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"
}

257
drivers/gofile/util.go Normal file
View File

@ -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
}