Skip to content

Commit fe564c4

Browse files
authored
feat: add pCloud driver support (#9339)
- Implement OAuth2 authentication with US/EU region support - Add file operations (list, upload, download, delete, rename, move, copy) - Add folder operations (create, rename, move, delete) - Enhance error handling with pCloud-specific retry logic - Use correct API methods: GET for reads, POST for writes - Implement direct upload approach for better performance - Add exponential backoff for failed requests with 4xxx/5xxx classification
1 parent d17889b commit fe564c4

File tree

5 files changed

+608
-0
lines changed

5 files changed

+608
-0
lines changed

drivers/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import (
5151
_ "github.com/alist-org/alist/v3/drivers/onedrive"
5252
_ "github.com/alist-org/alist/v3/drivers/onedrive_app"
5353
_ "github.com/alist-org/alist/v3/drivers/onedrive_sharelink"
54+
_ "github.com/alist-org/alist/v3/drivers/pcloud"
5455
_ "github.com/alist-org/alist/v3/drivers/pikpak"
5556
_ "github.com/alist-org/alist/v3/drivers/pikpak_share"
5657
_ "github.com/alist-org/alist/v3/drivers/quark_uc"

drivers/pcloud/driver.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package pcloud
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/alist-org/alist/v3/internal/driver"
8+
"github.com/alist-org/alist/v3/internal/model"
9+
"github.com/alist-org/alist/v3/pkg/utils"
10+
"github.com/go-resty/resty/v2"
11+
)
12+
13+
type PCloud struct {
14+
model.Storage
15+
Addition
16+
AccessToken string // Actual access token obtained from refresh token
17+
}
18+
19+
func (d *PCloud) Config() driver.Config {
20+
return config
21+
}
22+
23+
func (d *PCloud) GetAddition() driver.Additional {
24+
return &d.Addition
25+
}
26+
27+
func (d *PCloud) Init(ctx context.Context) error {
28+
// Map hostname selection to actual API endpoints
29+
if d.Hostname == "us" {
30+
d.Hostname = "api.pcloud.com"
31+
} else if d.Hostname == "eu" {
32+
d.Hostname = "eapi.pcloud.com"
33+
}
34+
35+
// Set default root folder ID if not provided
36+
if d.RootFolderID == "" {
37+
d.RootFolderID = "d0"
38+
}
39+
40+
// Use the access token directly (like rclone)
41+
d.AccessToken = d.RefreshToken // RefreshToken field actually contains the access_token
42+
return nil
43+
}
44+
45+
func (d *PCloud) Drop(ctx context.Context) error {
46+
return nil
47+
}
48+
49+
func (d *PCloud) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
50+
folderID := d.RootFolderID
51+
if dir.GetID() != "" {
52+
folderID = dir.GetID()
53+
}
54+
55+
files, err := d.getFiles(folderID)
56+
if err != nil {
57+
return nil, err
58+
}
59+
60+
return utils.SliceConvert(files, func(src FileObject) (model.Obj, error) {
61+
return fileToObj(src), nil
62+
})
63+
}
64+
65+
func (d *PCloud) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
66+
downloadURL, err := d.getDownloadLink(file.GetID())
67+
if err != nil {
68+
return nil, err
69+
}
70+
71+
return &model.Link{
72+
URL: downloadURL,
73+
}, nil
74+
}
75+
76+
// Mkdir implements driver.Mkdir
77+
func (d *PCloud) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
78+
parentID := d.RootFolderID
79+
if parentDir.GetID() != "" {
80+
parentID = parentDir.GetID()
81+
}
82+
83+
return d.createFolder(parentID, dirName)
84+
}
85+
86+
// Move implements driver.Move
87+
func (d *PCloud) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
88+
// pCloud uses renamefile/renamefolder for both rename and move
89+
endpoint := "/renamefile"
90+
paramName := "fileid"
91+
92+
if srcObj.IsDir() {
93+
endpoint = "/renamefolder"
94+
paramName = "folderid"
95+
}
96+
97+
var resp ItemResult
98+
_, err := d.requestWithRetry(endpoint, "POST", func(req *resty.Request) {
99+
req.SetFormData(map[string]string{
100+
paramName: extractID(srcObj.GetID()),
101+
"tofolderid": extractID(dstDir.GetID()),
102+
"toname": srcObj.GetName(),
103+
})
104+
}, &resp)
105+
106+
if err != nil {
107+
return err
108+
}
109+
110+
if resp.Result != 0 {
111+
return fmt.Errorf("pCloud error: result code %d", resp.Result)
112+
}
113+
114+
return nil
115+
}
116+
117+
// Rename implements driver.Rename
118+
func (d *PCloud) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
119+
endpoint := "/renamefile"
120+
paramName := "fileid"
121+
122+
if srcObj.IsDir() {
123+
endpoint = "/renamefolder"
124+
paramName = "folderid"
125+
}
126+
127+
var resp ItemResult
128+
_, err := d.requestWithRetry(endpoint, "POST", func(req *resty.Request) {
129+
req.SetFormData(map[string]string{
130+
paramName: extractID(srcObj.GetID()),
131+
"toname": newName,
132+
})
133+
}, &resp)
134+
135+
if err != nil {
136+
return err
137+
}
138+
139+
if resp.Result != 0 {
140+
return fmt.Errorf("pCloud error: result code %d", resp.Result)
141+
}
142+
143+
return nil
144+
}
145+
146+
// Copy implements driver.Copy
147+
func (d *PCloud) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
148+
endpoint := "/copyfile"
149+
paramName := "fileid"
150+
151+
if srcObj.IsDir() {
152+
endpoint = "/copyfolder"
153+
paramName = "folderid"
154+
}
155+
156+
var resp ItemResult
157+
_, err := d.requestWithRetry(endpoint, "POST", func(req *resty.Request) {
158+
req.SetFormData(map[string]string{
159+
paramName: extractID(srcObj.GetID()),
160+
"tofolderid": extractID(dstDir.GetID()),
161+
"toname": srcObj.GetName(),
162+
})
163+
}, &resp)
164+
165+
if err != nil {
166+
return err
167+
}
168+
169+
if resp.Result != 0 {
170+
return fmt.Errorf("pCloud error: result code %d", resp.Result)
171+
}
172+
173+
return nil
174+
}
175+
176+
// Remove implements driver.Remove
177+
func (d *PCloud) Remove(ctx context.Context, obj model.Obj) error {
178+
return d.delete(obj.GetID(), obj.IsDir())
179+
}
180+
181+
// Put implements driver.Put
182+
func (d *PCloud) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
183+
parentID := d.RootFolderID
184+
if dstDir.GetID() != "" {
185+
parentID = dstDir.GetID()
186+
}
187+
188+
return d.uploadFile(ctx, stream, parentID, stream.GetName(), stream.GetSize())
189+
}

drivers/pcloud/meta.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package pcloud
2+
3+
import (
4+
"github.com/alist-org/alist/v3/internal/driver"
5+
"github.com/alist-org/alist/v3/internal/op"
6+
)
7+
8+
type Addition struct {
9+
// Using json tag "access_token" for UI display, but internally it's a refresh token
10+
RefreshToken string `json:"access_token" required:"true" help:"OAuth token from pCloud authorization"`
11+
Hostname string `json:"hostname" type:"select" options:"us,eu" default:"us" help:"Select pCloud server region"`
12+
RootFolderID string `json:"root_folder_id" help:"Get folder ID from URL like https://my.pcloud.com/#/filemanager?folder=12345678901 (leave empty for root folder)"`
13+
ClientID string `json:"client_id" help:"Custom OAuth client ID (optional)"`
14+
ClientSecret string `json:"client_secret" help:"Custom OAuth client secret (optional)"`
15+
}
16+
17+
// Implement IRootId interface
18+
func (a Addition) GetRootId() string {
19+
return a.RootFolderID
20+
}
21+
22+
var config = driver.Config{
23+
Name: "pCloud",
24+
}
25+
26+
func init() {
27+
op.RegisterDriver(func() driver.Driver {
28+
return &PCloud{}
29+
})
30+
}

drivers/pcloud/types.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package pcloud
2+
3+
import (
4+
"strconv"
5+
"time"
6+
7+
"github.com/alist-org/alist/v3/internal/model"
8+
)
9+
10+
// ErrorResult represents a pCloud API error response
11+
type ErrorResult struct {
12+
Result int `json:"result"`
13+
Error string `json:"error"`
14+
}
15+
16+
// TokenResponse represents OAuth token response
17+
type TokenResponse struct {
18+
AccessToken string `json:"access_token"`
19+
TokenType string `json:"token_type"`
20+
}
21+
22+
// ItemResult represents a common pCloud API response
23+
type ItemResult struct {
24+
Result int `json:"result"`
25+
Metadata *FolderMeta `json:"metadata,omitempty"`
26+
}
27+
28+
// FolderMeta contains folder metadata including contents
29+
type FolderMeta struct {
30+
Contents []FileObject `json:"contents,omitempty"`
31+
}
32+
33+
// DownloadLinkResult represents download link response
34+
type DownloadLinkResult struct {
35+
Result int `json:"result"`
36+
Hosts []string `json:"hosts"`
37+
Path string `json:"path"`
38+
}
39+
40+
// FileObject represents a file or folder object in pCloud
41+
type FileObject struct {
42+
Name string `json:"name"`
43+
Created string `json:"created"` // pCloud returns RFC1123 format string
44+
Modified string `json:"modified"` // pCloud returns RFC1123 format string
45+
IsFolder bool `json:"isfolder"`
46+
FolderID uint64 `json:"folderid,omitempty"`
47+
FileID uint64 `json:"fileid,omitempty"`
48+
Size uint64 `json:"size"`
49+
ParentID uint64 `json:"parentfolderid"`
50+
Icon string `json:"icon,omitempty"`
51+
Hash uint64 `json:"hash,omitempty"`
52+
Category int `json:"category,omitempty"`
53+
ID string `json:"id,omitempty"`
54+
}
55+
56+
// Convert FileObject to model.Obj
57+
func fileToObj(f FileObject) model.Obj {
58+
// Parse RFC1123 format time from pCloud
59+
modTime, _ := time.Parse(time.RFC1123, f.Modified)
60+
61+
obj := model.Object{
62+
Name: f.Name,
63+
Size: int64(f.Size),
64+
Modified: modTime,
65+
IsFolder: f.IsFolder,
66+
}
67+
68+
if f.IsFolder {
69+
obj.ID = "d" + strconv.FormatUint(f.FolderID, 10)
70+
} else {
71+
obj.ID = "f" + strconv.FormatUint(f.FileID, 10)
72+
}
73+
74+
return &obj
75+
}
76+
77+
// Extract numeric ID from string ID (remove 'd' or 'f' prefix)
78+
func extractID(id string) string {
79+
if len(id) > 1 && (id[0] == 'd' || id[0] == 'f') {
80+
return id[1:]
81+
}
82+
return id
83+
}
84+
85+
// Get folder ID from path, return "0" for root
86+
func getFolderID(path string) string {
87+
if path == "/" || path == "" {
88+
return "0"
89+
}
90+
return extractID(path)
91+
}

0 commit comments

Comments
 (0)