Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions drivers/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import (
_ "github.com/alist-org/alist/v3/drivers/onedrive"
_ "github.com/alist-org/alist/v3/drivers/onedrive_app"
_ "github.com/alist-org/alist/v3/drivers/onedrive_sharelink"
_ "github.com/alist-org/alist/v3/drivers/pcloud"
_ "github.com/alist-org/alist/v3/drivers/pikpak"
_ "github.com/alist-org/alist/v3/drivers/pikpak_share"
_ "github.com/alist-org/alist/v3/drivers/quark_uc"
Expand Down
189 changes: 189 additions & 0 deletions drivers/pcloud/driver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package pcloud

import (
"context"
"fmt"

"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
)

type PCloud struct {
model.Storage
Addition
AccessToken string // Actual access token obtained from refresh token
}

func (d *PCloud) Config() driver.Config {
return config
}

func (d *PCloud) GetAddition() driver.Additional {
return &d.Addition
}

func (d *PCloud) Init(ctx context.Context) error {
// Map hostname selection to actual API endpoints
if d.Hostname == "us" {
d.Hostname = "api.pcloud.com"
} else if d.Hostname == "eu" {
d.Hostname = "eapi.pcloud.com"
}

// Set default root folder ID if not provided
if d.RootFolderID == "" {
d.RootFolderID = "d0"
}

// Use the access token directly (like rclone)
d.AccessToken = d.RefreshToken // RefreshToken field actually contains the access_token
return nil
}

func (d *PCloud) Drop(ctx context.Context) error {
return nil
}

func (d *PCloud) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
folderID := d.RootFolderID
if dir.GetID() != "" {
folderID = dir.GetID()
}

files, err := d.getFiles(folderID)
if err != nil {
return nil, err
}

return utils.SliceConvert(files, func(src FileObject) (model.Obj, error) {
return fileToObj(src), nil
})
}

func (d *PCloud) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
downloadURL, err := d.getDownloadLink(file.GetID())
if err != nil {
return nil, err
}

return &model.Link{
URL: downloadURL,
}, nil
}

// Mkdir implements driver.Mkdir
func (d *PCloud) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
parentID := d.RootFolderID
if parentDir.GetID() != "" {
parentID = parentDir.GetID()
}

return d.createFolder(parentID, dirName)
}

// Move implements driver.Move
func (d *PCloud) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
// pCloud uses renamefile/renamefolder for both rename and move
endpoint := "/renamefile"
paramName := "fileid"

if srcObj.IsDir() {
endpoint = "/renamefolder"
paramName = "folderid"
}

var resp ItemResult
_, err := d.requestWithRetry(endpoint, "POST", func(req *resty.Request) {
req.SetFormData(map[string]string{
paramName: extractID(srcObj.GetID()),
"tofolderid": extractID(dstDir.GetID()),
"toname": srcObj.GetName(),
})
}, &resp)

if err != nil {
return err
}

if resp.Result != 0 {
return fmt.Errorf("pCloud error: result code %d", resp.Result)
}

return nil
}

// Rename implements driver.Rename
func (d *PCloud) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
endpoint := "/renamefile"
paramName := "fileid"

if srcObj.IsDir() {
endpoint = "/renamefolder"
paramName = "folderid"
}

var resp ItemResult
_, err := d.requestWithRetry(endpoint, "POST", func(req *resty.Request) {
req.SetFormData(map[string]string{
paramName: extractID(srcObj.GetID()),
"toname": newName,
})
}, &resp)

if err != nil {
return err
}

if resp.Result != 0 {
return fmt.Errorf("pCloud error: result code %d", resp.Result)
}

return nil
}

// Copy implements driver.Copy
func (d *PCloud) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
endpoint := "/copyfile"
paramName := "fileid"

if srcObj.IsDir() {
endpoint = "/copyfolder"
paramName = "folderid"
}

var resp ItemResult
_, err := d.requestWithRetry(endpoint, "POST", func(req *resty.Request) {
req.SetFormData(map[string]string{
paramName: extractID(srcObj.GetID()),
"tofolderid": extractID(dstDir.GetID()),
"toname": srcObj.GetName(),
})
}, &resp)

if err != nil {
return err
}

if resp.Result != 0 {
return fmt.Errorf("pCloud error: result code %d", resp.Result)
}

return nil
}

// Remove implements driver.Remove
func (d *PCloud) Remove(ctx context.Context, obj model.Obj) error {
return d.delete(obj.GetID(), obj.IsDir())
}

// Put implements driver.Put
func (d *PCloud) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
parentID := d.RootFolderID
if dstDir.GetID() != "" {
parentID = dstDir.GetID()
}

return d.uploadFile(ctx, stream, parentID, stream.GetName(), stream.GetSize())
}
30 changes: 30 additions & 0 deletions drivers/pcloud/meta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package pcloud

import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)

type Addition struct {
// Using json tag "access_token" for UI display, but internally it's a refresh token
RefreshToken string `json:"access_token" required:"true" help:"OAuth token from pCloud authorization"`
Hostname string `json:"hostname" type:"select" options:"us,eu" default:"us" help:"Select pCloud server region"`
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)"`
ClientID string `json:"client_id" help:"Custom OAuth client ID (optional)"`
ClientSecret string `json:"client_secret" help:"Custom OAuth client secret (optional)"`
}

// Implement IRootId interface
func (a Addition) GetRootId() string {
return a.RootFolderID
}

var config = driver.Config{
Name: "pCloud",
}

func init() {
op.RegisterDriver(func() driver.Driver {
return &PCloud{}
})
}
91 changes: 91 additions & 0 deletions drivers/pcloud/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package pcloud

import (
"strconv"
"time"

"github.com/alist-org/alist/v3/internal/model"
)

// ErrorResult represents a pCloud API error response
type ErrorResult struct {
Result int `json:"result"`
Error string `json:"error"`
}

// TokenResponse represents OAuth token response
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
}

// ItemResult represents a common pCloud API response
type ItemResult struct {
Result int `json:"result"`
Metadata *FolderMeta `json:"metadata,omitempty"`
}

// FolderMeta contains folder metadata including contents
type FolderMeta struct {
Contents []FileObject `json:"contents,omitempty"`
}

// DownloadLinkResult represents download link response
type DownloadLinkResult struct {
Result int `json:"result"`
Hosts []string `json:"hosts"`
Path string `json:"path"`
}

// FileObject represents a file or folder object in pCloud
type FileObject struct {
Name string `json:"name"`
Created string `json:"created"` // pCloud returns RFC1123 format string
Modified string `json:"modified"` // pCloud returns RFC1123 format string
IsFolder bool `json:"isfolder"`
FolderID uint64 `json:"folderid,omitempty"`
FileID uint64 `json:"fileid,omitempty"`
Size uint64 `json:"size"`
ParentID uint64 `json:"parentfolderid"`
Icon string `json:"icon,omitempty"`
Hash uint64 `json:"hash,omitempty"`
Category int `json:"category,omitempty"`
ID string `json:"id,omitempty"`
}

// Convert FileObject to model.Obj
func fileToObj(f FileObject) model.Obj {
// Parse RFC1123 format time from pCloud
modTime, _ := time.Parse(time.RFC1123, f.Modified)

obj := model.Object{
Name: f.Name,
Size: int64(f.Size),
Modified: modTime,
IsFolder: f.IsFolder,
}

if f.IsFolder {
obj.ID = "d" + strconv.FormatUint(f.FolderID, 10)
} else {
obj.ID = "f" + strconv.FormatUint(f.FileID, 10)
}

return &obj
}

// Extract numeric ID from string ID (remove 'd' or 'f' prefix)
func extractID(id string) string {
if len(id) > 1 && (id[0] == 'd' || id[0] == 'f') {
return id[1:]
}
return id
}

// Get folder ID from path, return "0" for root
func getFolderID(path string) string {
if path == "/" || path == "" {
return "0"
}
return extractID(path)
}
Loading