Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
bc36afc
feat(drivers): add ProtonDrive driver
Da3zKi7 Sep 26, 2025
eaa95ea
Update drivers/proton_drive/util.go
Da3zKi7 Sep 28, 2025
a209a85
chore
KirCute Sep 29, 2025
517f4ee
Merge branch 'main' into feat/proton-drive
Da3zKi7 Sep 30, 2025
8caefc3
feat(drivers): enhance ProtonDrive temp server
Da3zKi7 Sep 30, 2025
68824b5
Merge branch 'main' into feat/proton-drive
Da3zKi7 Sep 30, 2025
32b745a
Merge branch 'main' into feat/proton-drive
Da3zKi7 Oct 2, 2025
f28cc4d
Merge branch 'main' into feat/proton-drive
Da3zKi7 Oct 6, 2025
ccb9298
Merge branch 'main' into feat/proton-drive
Da3zKi7 Oct 6, 2025
386961d
feat(proton_drive): refactor directory handling and improve link retr…
j2rong4cn Oct 10, 2025
564d252
fix(proton_drive): add NoLinkURL configuration option
j2rong4cn Oct 10, 2025
3d99787
fix(proton_drive): update file size retrieval and enforce TwoFACode r…
j2rong4cn Oct 10, 2025
363624b
feat(proton_drive): add expiration to link response
j2rong4cn Oct 10, 2025
9753fde
fix(proton_drive): handle empty RootFolderID in Init method
j2rong4cn Oct 10, 2025
e867092
fix(proton_drive): update credential handling to use email and reusab…
j2rong4cn Oct 10, 2025
9d5a78a
fix(proton_drive): update credential handling to use reusableCredenti…
j2rong4cn Oct 10, 2025
6254480
fix(proton_drive): update DirectRename to use GetLink for source obje…
j2rong4cn Oct 11, 2025
d7ce484
fix(proton_drive): refactor uploadFile to return model.Obj and handle…
j2rong4cn Oct 11, 2025
493b96c
fix(proton_drive): refactor DirectMove to use getLink for source retr…
j2rong4cn Oct 11, 2025
404879f
fix(proton_drive): simplify Copy method by removing temporary file cr…
j2rong4cn Oct 11, 2025
2e36284
refactor(proton_drive): remove unused temporary server and related code
j2rong4cn Oct 11, 2025
f81cc1b
chore
KirCute Oct 11, 2025
8868cf7
fix(proton_drive): fix driver
Da3zKi7 Oct 13, 2025
9341c8b
fix(proton_drive): simplify reusable login handling in Init method
j2rong4cn Oct 13, 2025
7500971
Merge branch 'main' into feat/proton-drive
Da3zKi7 Oct 14, 2025
2fa6b05
fix(proton_drive): fix driver
Da3zKi7 Oct 14, 2025
ce45387
feat(proton_drive): improve authentication handling and remove unused…
j2rong4cn Oct 14, 2025
855aab1
fix(proton_drive): fix driver
Da3zKi7 Oct 14, 2025
554dad0
fix(proton_drive): improve authentication handling
Da3zKi7 Oct 14, 2025
f29c58d
refactor(proton_drive): move client initialization to initClient method
j2rong4cn Oct 14, 2025
0b84a1c
feat(proton_drive): move addrs and addrKRs
Da3zKi7 Oct 15, 2025
fdee792
feat(proton_drive): optimize upload threads
Da3zKi7 Oct 15, 2025
889077e
Merge branch 'main' into feat/proton-drive
Da3zKi7 Oct 17, 2025
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ Thank you for your support and understanding of the OpenList project.
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
- [x] Teambition([China](https://www.teambition.com), [International](https://us.teambition.com))
- [x] [Mediatrack](https://www.mediatrack.cn)
- [x] [MediaFire](https://www.mediafire.com)
- [x] [Mediatrack](https://www.mediatrack.cn)
- [x] [ProtonDrive](https://proton.me/drive)
- [x] [139yun](https://yun.139.com) (Personal, Family, Group)
- [x] [YandexDisk](https://disk.yandex.com)
- [x] [BaiduNetdisk](http://pan.baidu.com)
Expand Down
3 changes: 2 additions & 1 deletion README_cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ OpenList 是一个由 OpenList 团队独立维护的开源项目,遵循 AGPL-3
- [x] [又拍云对象存储](https://www.upyun.com/products/file-storage)
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
- [x] Teambition([中国](https://www.teambition.com), [国际](https://us.teambition.com))
- [x] [分秒帧](https://www.mediatrack.cn)
- [x] [MediaFire](https://www.mediafire.com)
- [x] [分秒帧](https://www.mediatrack.cn)
- [x] [ProtonDrive](https://proton.me/drive)
- [x] [和彩云](https://yun.139.com)(个人、家庭、群组)
- [x] [YandexDisk](https://disk.yandex.com)
- [x] [百度网盘](http://pan.baidu.com)
Expand Down
1 change: 1 addition & 0 deletions README_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ OpenListプロジェクトへのご支援とご理解をありがとうござい
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
- [x] Teambition([中国](https://www.teambition.com), [国際](https://us.teambition.com))
- [x] [Mediatrack](https://www.mediatrack.cn)
- [x] [ProtonDrive](https://proton.me/drive)
- [x] [139yun](https://yun.139.com)(個人、家族、グループ)
- [x] [YandexDisk](https://disk.yandex.com)
- [x] [BaiduNetdisk](http://pan.baidu.com)
Expand Down
1 change: 1 addition & 0 deletions README_nl.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Dank u voor uw ondersteuning en begrip
- [x] Teambition([China](https://www.teambition.com), [Internationaal](https://us.teambition.com))
- [x] [MediaFire](https://www.mediafire.com)
- [x] [Mediatrack](https://www.mediatrack.cn)
- [x] [ProtonDrive](https://proton.me/drive)
- [x] [139yun](https://yun.139.com) (Persoonlijk, Familie, Groep)
- [x] [YandexDisk](https://disk.yandex.com)
- [x] [BaiduNetdisk](http://pan.baidu.com)
Expand Down
1 change: 1 addition & 0 deletions drivers/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import (
_ "github.com/OpenListTeam/OpenList/v4/drivers/openlist_share"
_ "github.com/OpenListTeam/OpenList/v4/drivers/pikpak"
_ "github.com/OpenListTeam/OpenList/v4/drivers/pikpak_share"
_ "github.com/OpenListTeam/OpenList/v4/drivers/proton_drive"
_ "github.com/OpenListTeam/OpenList/v4/drivers/quark_open"
_ "github.com/OpenListTeam/OpenList/v4/drivers/quark_uc"
_ "github.com/OpenListTeam/OpenList/v4/drivers/quark_uc_tv"
Expand Down
290 changes: 290 additions & 0 deletions drivers/proton_drive/driver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
package protondrive

/*
Package protondrive
Author: Da3zKi7<da3zki7@duck.com>
Date: 2025-09-18

Thanks to @henrybear327 for modded go-proton-api & Proton-API-Bridge

The power of open-source, the force of teamwork and the magic of reverse engineering!


D@' 3z K!7 - The King Of Cracking

Да здравствует Родина))
*/

import (
"context"
"fmt"
"io"
"time"

"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/internal/setting"
"github.com/OpenListTeam/OpenList/v4/internal/stream"
"github.com/OpenListTeam/OpenList/v4/pkg/http_range"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/ProtonMail/gopenpgp/v2/crypto"
proton_api_bridge "github.com/henrybear327/Proton-API-Bridge"
"github.com/henrybear327/Proton-API-Bridge/common"
"github.com/henrybear327/go-proton-api"
)

type ProtonDrive struct {
model.Storage
Addition

protonDrive *proton_api_bridge.ProtonDrive

apiBase string
appVersion string
protonJson string
userAgent string
sdkVersion string
webDriveAV string

c *proton.Client

// userKR *crypto.KeyRing
addrKRs map[string]*crypto.KeyRing
addrData map[string]proton.Address

MainShare *proton.Share

DefaultAddrKR *crypto.KeyRing
MainShareKR *crypto.KeyRing
}

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

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

func (d *ProtonDrive) Init(ctx context.Context) (err error) {
defer func() {
if r := recover(); err == nil && r != nil {
err = fmt.Errorf("ProtonDrive initialization panic: %v", r)
}
}()

if d.Email == "" {
return fmt.Errorf("email is required")
}
if d.Password == "" {
return fmt.Errorf("password is required")
}

config := &common.Config{
AppVersion: d.appVersion,
UserAgent: d.userAgent,
FirstLoginCredential: &common.FirstLoginCredentialData{
Username: d.Email,
Password: d.Password,
TwoFA: d.TwoFACode,
},
EnableCaching: true,
ConcurrentBlockUploadCount: setting.GetInt(conf.TaskUploadThreadsNum, conf.Conf.Tasks.Upload.Workers),
//ConcurrentFileCryptoCount: 2,
UseReusableLogin: d.UseReusableLogin && d.ReusableCredential != (common.ReusableCredentialData{}),
ReplaceExistingDraft: true,
ReusableCredential: &d.ReusableCredential,
}

protonDrive, _, err := proton_api_bridge.NewProtonDrive(
ctx,
config,
d.authHandler,
func() {},
)

if err != nil && config.UseReusableLogin {
config.UseReusableLogin = false
protonDrive, _, err = proton_api_bridge.NewProtonDrive(ctx,
config,
d.authHandler,
func() {},
)
if err == nil {
op.MustSaveDriverStorage(d)
}
}

if err != nil {
return fmt.Errorf("failed to initialize ProtonDrive: %w", err)
}

if err := d.initClient(ctx); err != nil {
return err
}

d.protonDrive = protonDrive
d.MainShare = protonDrive.MainShare
if d.RootFolderID == "root" || d.RootFolderID == "" {
d.RootFolderID = protonDrive.RootLink.LinkID
}
d.MainShareKR = protonDrive.MainShareKR
d.DefaultAddrKR = protonDrive.DefaultAddrKR

return nil
}

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

func (d *ProtonDrive) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
entries, err := d.protonDrive.ListDirectory(ctx, dir.GetID())
if err != nil {
return nil, fmt.Errorf("failed to list directory: %w", err)
}

objects := make([]model.Obj, 0, len(entries))
for _, entry := range entries {
obj := &model.Object{
ID: entry.Link.LinkID,
Name: entry.Name,
Size: entry.Link.Size,
Modified: time.Unix(entry.Link.ModifyTime, 0),
IsFolder: entry.IsFolder,
}
objects = append(objects, obj)
}

return objects, nil
}

func (d *ProtonDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
link, err := d.getLink(ctx, file.GetID())
if err != nil {
return nil, fmt.Errorf("failed get file link: %+v", err)
}
fileSystemAttrs, err := d.protonDrive.GetActiveRevisionAttrs(ctx, link)
if err != nil {
return nil, fmt.Errorf("failed get file revision: %+v", err)
}
// 解密后的文件大小
size := fileSystemAttrs.Size

rangeReaderFunc := func(rangeCtx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
length := httpRange.Length
if length < 0 || httpRange.Start+length > size {
length = size - httpRange.Start
}
reader, _, _, err := d.protonDrive.DownloadFile(rangeCtx, link, httpRange.Start)
if err != nil {
return nil, fmt.Errorf("failed start download: %+v", err)
}
return utils.ReadCloser{
Reader: io.LimitReader(reader, length),
Closer: reader,
}, nil
}

expiration := time.Minute
return &model.Link{
RangeReader: &model.FileRangeReader{
RangeReaderIF: stream.RateLimitRangeReaderFunc(rangeReaderFunc),
},
ContentLength: size,
Expiration: &expiration,
}, nil
}

func (d *ProtonDrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
id, err := d.protonDrive.CreateNewFolderByID(ctx, parentDir.GetID(), dirName)
if err != nil {
return nil, fmt.Errorf("failed to create directory: %w", err)
}

newDir := &model.Object{
ID: id,
Name: dirName,
IsFolder: true,
Modified: time.Now(),
}
return newDir, nil
}

func (d *ProtonDrive) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
return d.DirectMove(ctx, srcObj, dstDir)
}

func (d *ProtonDrive) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
if d.protonDrive == nil {
return nil, fmt.Errorf("protonDrive bridge is nil")
}

return d.DirectRename(ctx, srcObj, newName)
}

func (d *ProtonDrive) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
if srcObj.IsDir() {
return nil, fmt.Errorf("directory copy not supported")
}

srcLink, err := d.getLink(ctx, srcObj.GetID())
if err != nil {
return nil, err
}

reader, linkSize, fileSystemAttrs, err := d.protonDrive.DownloadFile(ctx, srcLink, 0)
if err != nil {
return nil, fmt.Errorf("failed to download source file: %w", err)
}
defer reader.Close()

actualSize := linkSize
if fileSystemAttrs != nil && fileSystemAttrs.Size > 0 {
actualSize = fileSystemAttrs.Size
}

file := &stream.FileStream{
Ctx: ctx,
Obj: &model.Object{
Name: srcObj.GetName(),
// Use the accurate and real size
Size: actualSize,
Modified: srcObj.ModTime(),
},
Reader: reader,
}
defer file.Close()
return d.Put(ctx, dstDir, file, func(percentage float64) {})
}

func (d *ProtonDrive) Remove(ctx context.Context, obj model.Obj) error {
if obj.IsDir() {
return d.protonDrive.MoveFolderToTrashByID(ctx, obj.GetID(), false)
} else {
return d.protonDrive.MoveFileToTrashByID(ctx, obj.GetID())
}
}

func (d *ProtonDrive) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
return d.uploadFile(ctx, dstDir.GetID(), file, up)
}

func (d *ProtonDrive) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
about, err := d.protonDrive.About(ctx)
if err != nil {
return nil, err
}
total := uint64(about.MaxSpace)
free := total - uint64(about.UsedSpace)
return &model.StorageDetails{
DiskUsage: model.DiskUsage{
TotalSpace: total,
FreeSpace: free,
},
}, nil
}

var _ driver.Driver = (*ProtonDrive)(nil)
56 changes: 56 additions & 0 deletions drivers/proton_drive/meta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package protondrive

/*
Package protondrive
Author: Da3zKi7<da3zki7@duck.com>
Date: 2025-09-18

Thanks to @henrybear327 for modded go-proton-api & Proton-API-Bridge

The power of open-source, the force of teamwork and the magic of reverse engineering!


D@' 3z K!7 - The King Of Cracking

Да здравствует Родина))
*/

import (
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/henrybear327/Proton-API-Bridge/common"
)

type Addition struct {
driver.RootID
Email string `json:"email" required:"true" type:"string"`
Password string `json:"password" required:"true" type:"string"`
TwoFACode string `json:"two_fa_code" type:"string"`
ChunkSize int64 `json:"chunk_size" type:"number" default:"100"`
UseReusableLogin bool `json:"use_reusable_login" type:"bool" default:"true" help:"Use reusable login credentials instead of username/password"`
ReusableCredential common.ReusableCredentialData
}

var config = driver.Config{
Name: "ProtonDrive",
LocalSort: true,
OnlyProxy: true,
DefaultRoot: "root",
NoLinkURL: true,
}

func init() {
op.RegisterDriver(func() driver.Driver {
return &ProtonDrive{
Addition: Addition{
UseReusableLogin: true,
},
apiBase: "https://drive.proton.me/api",
appVersion: "windows-drive@1.11.3+rclone+proton",
protonJson: "application/vnd.protonmail.v1+json",
sdkVersion: "js@0.3.0",
userAgent: "ProtonDrive/v1.70.0 (Windows NT 10.0.22000; Win64; x64)",
webDriveAV: "web-drive@5.2.0+0f69f7a8",
}
})
}
Loading