Skip to content

Commit ae2d2d1

Browse files
Da3zKi7CopilotKirCutej2rong4cn
authored
feat(drivers): add ProtonDrive driver (#1368)
* feat(drivers): add ProtonDrive driver - Implement complete ProtonDrive storage driver with end-to-end encryption support - Add authentication via username/password with credential caching and reusable login - Support all core operations: List, Link, Put, Copy, Move, Remove, Rename, MakeDir - Include encrypted file operations with PGP key management and node passphrase handling - Add temporary HTTP server for secure file downloads with range request support - Support media streaming using temp server range requests - Implement progress tracking for uploads and downloads - Support directory operations with circular move detection - Add proper error handling and panic recovery for external library integration - Support buffered upload for specific sequential and encrypted, but optimized transmission. * Update drivers/proton_drive/util.go Co-authored-by: Copilot <[email protected]> Signed-off-by: D@' 3z K!7 <[email protected]> * chore * feat(drivers): enhance ProtonDrive temp server - Implement separate listen and public port configuration for complex network deployments - Add intelligent port detection with 8080 as preferred default, fallback to auto-assignment - Support Container/NAT/VM environments through configurable external host and port mapping - Add port availability validation with graceful fallback to listen port - Enable users to specify external domain/IP for client connections (e.g., 192.168.1.5) - Follow FTP server configuration patterns for network flexibility - Maintain localhost development simplicity while supporting production deployments * feat(proton_drive): refactor directory handling and improve link retrieval * fix(proton_drive): add NoLinkURL configuration option * fix(proton_drive): update file size retrieval and enforce TwoFACode requirement * feat(proton_drive): add expiration to link response * fix(proton_drive): handle empty RootFolderID in Init method * fix(proton_drive): update credential handling to use email and reusable login * fix(proton_drive): update credential handling to use reusableCredential variable * fix(proton_drive): update DirectRename to use GetLink for source object retrieval * fix(proton_drive): refactor uploadFile to return model.Obj and handle errors correctly * fix(proton_drive): refactor DirectMove to use getLink for source retrieval and simplify destination handling * fix(proton_drive): simplify Copy method by removing temporary file creation and directly using FileStream * refactor(proton_drive): remove unused temporary server and related code * chore * fix(proton_drive): fix driver - Handle fresh login if ProtonDrive rejects AccessToken or RefreshToken - Update stored credentials * fix(proton_drive): simplify reusable login handling in Init method * fix(proton_drive): fix driver - Update stored credentials, now is failing * feat(proton_drive): improve authentication handling and remove unused variables * fix(proton_drive): fix driver - Update stored credentials, now is failing * fix(proton_drive): improve authentication handling * refactor(proton_drive): move client initialization to initClient method * feat(proton_drive): move addrs and addrKRs * feat(proton_drive): optimize upload threads - Change ConcurrentBlockUploadCount to user configured upload threads number - Comment ConcurrentFileCryptoCount, default is runtime.GOMAXPROCS(0) --------- Signed-off-by: D@' 3z K!7 <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: KirCute <[email protected]> Co-authored-by: j2rong4cn <[email protected]> Co-authored-by: KirCute <[email protected]>
1 parent a109152 commit ae2d2d1

File tree

11 files changed

+1121
-2
lines changed

11 files changed

+1121
-2
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ Thank you for your support and understanding of the OpenList project.
6464
- [x] [UPYUN Storage Service](https://www.upyun.com/products/file-storage)
6565
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
6666
- [x] Teambition([China](https://www.teambition.com), [International](https://us.teambition.com))
67-
- [x] [Mediatrack](https://www.mediatrack.cn)
6867
- [x] [MediaFire](https://www.mediafire.com)
68+
- [x] [Mediatrack](https://www.mediatrack.cn)
69+
- [x] [ProtonDrive](https://proton.me/drive)
6970
- [x] [139yun](https://yun.139.com) (Personal, Family, Group)
7071
- [x] [YandexDisk](https://disk.yandex.com)
7172
- [x] [BaiduNetdisk](http://pan.baidu.com)

README_cn.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ OpenList 是一个由 OpenList 团队独立维护的开源项目,遵循 AGPL-3
6464
- [x] [又拍云对象存储](https://www.upyun.com/products/file-storage)
6565
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
6666
- [x] Teambition([中国](https://www.teambition.com), [国际](https://us.teambition.com))
67-
- [x] [分秒帧](https://www.mediatrack.cn)
6867
- [x] [MediaFire](https://www.mediafire.com)
68+
- [x] [分秒帧](https://www.mediatrack.cn)
69+
- [x] [ProtonDrive](https://proton.me/drive)
6970
- [x] [和彩云](https://yun.139.com)(个人、家庭、群组)
7071
- [x] [YandexDisk](https://disk.yandex.com)
7172
- [x] [百度网盘](http://pan.baidu.com)

README_ja.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ OpenListプロジェクトへのご支援とご理解をありがとうござい
6565
- [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV)
6666
- [x] Teambition([中国](https://www.teambition.com), [国際](https://us.teambition.com))
6767
- [x] [Mediatrack](https://www.mediatrack.cn)
68+
- [x] [ProtonDrive](https://proton.me/drive)
6869
- [x] [139yun](https://yun.139.com)(個人、家族、グループ)
6970
- [x] [YandexDisk](https://disk.yandex.com)
7071
- [x] [BaiduNetdisk](http://pan.baidu.com)

README_nl.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ Dank u voor uw ondersteuning en begrip
6666
- [x] Teambition([China](https://www.teambition.com), [Internationaal](https://us.teambition.com))
6767
- [x] [MediaFire](https://www.mediafire.com)
6868
- [x] [Mediatrack](https://www.mediatrack.cn)
69+
- [x] [ProtonDrive](https://proton.me/drive)
6970
- [x] [139yun](https://yun.139.com) (Persoonlijk, Familie, Groep)
7071
- [x] [YandexDisk](https://disk.yandex.com)
7172
- [x] [BaiduNetdisk](http://pan.baidu.com)

drivers/all.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import (
5656
_ "github.com/OpenListTeam/OpenList/v4/drivers/openlist_share"
5757
_ "github.com/OpenListTeam/OpenList/v4/drivers/pikpak"
5858
_ "github.com/OpenListTeam/OpenList/v4/drivers/pikpak_share"
59+
_ "github.com/OpenListTeam/OpenList/v4/drivers/proton_drive"
5960
_ "github.com/OpenListTeam/OpenList/v4/drivers/quark_open"
6061
_ "github.com/OpenListTeam/OpenList/v4/drivers/quark_uc"
6162
_ "github.com/OpenListTeam/OpenList/v4/drivers/quark_uc_tv"

drivers/proton_drive/driver.go

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
package protondrive
2+
3+
/*
4+
Package protondrive
5+
Author: Da3zKi7<[email protected]>
6+
Date: 2025-09-18
7+
8+
Thanks to @henrybear327 for modded go-proton-api & Proton-API-Bridge
9+
10+
The power of open-source, the force of teamwork and the magic of reverse engineering!
11+
12+
13+
D@' 3z K!7 - The King Of Cracking
14+
15+
Да здравствует Родина))
16+
*/
17+
18+
import (
19+
"context"
20+
"fmt"
21+
"io"
22+
"time"
23+
24+
"github.com/OpenListTeam/OpenList/v4/internal/conf"
25+
"github.com/OpenListTeam/OpenList/v4/internal/driver"
26+
"github.com/OpenListTeam/OpenList/v4/internal/model"
27+
"github.com/OpenListTeam/OpenList/v4/internal/op"
28+
"github.com/OpenListTeam/OpenList/v4/internal/setting"
29+
"github.com/OpenListTeam/OpenList/v4/internal/stream"
30+
"github.com/OpenListTeam/OpenList/v4/pkg/http_range"
31+
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
32+
"github.com/ProtonMail/gopenpgp/v2/crypto"
33+
proton_api_bridge "github.com/henrybear327/Proton-API-Bridge"
34+
"github.com/henrybear327/Proton-API-Bridge/common"
35+
"github.com/henrybear327/go-proton-api"
36+
)
37+
38+
type ProtonDrive struct {
39+
model.Storage
40+
Addition
41+
42+
protonDrive *proton_api_bridge.ProtonDrive
43+
44+
apiBase string
45+
appVersion string
46+
protonJson string
47+
userAgent string
48+
sdkVersion string
49+
webDriveAV string
50+
51+
c *proton.Client
52+
53+
// userKR *crypto.KeyRing
54+
addrKRs map[string]*crypto.KeyRing
55+
addrData map[string]proton.Address
56+
57+
MainShare *proton.Share
58+
59+
DefaultAddrKR *crypto.KeyRing
60+
MainShareKR *crypto.KeyRing
61+
}
62+
63+
func (d *ProtonDrive) Config() driver.Config {
64+
return config
65+
}
66+
67+
func (d *ProtonDrive) GetAddition() driver.Additional {
68+
return &d.Addition
69+
}
70+
71+
func (d *ProtonDrive) Init(ctx context.Context) (err error) {
72+
defer func() {
73+
if r := recover(); err == nil && r != nil {
74+
err = fmt.Errorf("ProtonDrive initialization panic: %v", r)
75+
}
76+
}()
77+
78+
if d.Email == "" {
79+
return fmt.Errorf("email is required")
80+
}
81+
if d.Password == "" {
82+
return fmt.Errorf("password is required")
83+
}
84+
85+
config := &common.Config{
86+
AppVersion: d.appVersion,
87+
UserAgent: d.userAgent,
88+
FirstLoginCredential: &common.FirstLoginCredentialData{
89+
Username: d.Email,
90+
Password: d.Password,
91+
TwoFA: d.TwoFACode,
92+
},
93+
EnableCaching: true,
94+
ConcurrentBlockUploadCount: setting.GetInt(conf.TaskUploadThreadsNum, conf.Conf.Tasks.Upload.Workers),
95+
//ConcurrentFileCryptoCount: 2,
96+
UseReusableLogin: d.UseReusableLogin && d.ReusableCredential != (common.ReusableCredentialData{}),
97+
ReplaceExistingDraft: true,
98+
ReusableCredential: &d.ReusableCredential,
99+
}
100+
101+
protonDrive, _, err := proton_api_bridge.NewProtonDrive(
102+
ctx,
103+
config,
104+
d.authHandler,
105+
func() {},
106+
)
107+
108+
if err != nil && config.UseReusableLogin {
109+
config.UseReusableLogin = false
110+
protonDrive, _, err = proton_api_bridge.NewProtonDrive(ctx,
111+
config,
112+
d.authHandler,
113+
func() {},
114+
)
115+
if err == nil {
116+
op.MustSaveDriverStorage(d)
117+
}
118+
}
119+
120+
if err != nil {
121+
return fmt.Errorf("failed to initialize ProtonDrive: %w", err)
122+
}
123+
124+
if err := d.initClient(ctx); err != nil {
125+
return err
126+
}
127+
128+
d.protonDrive = protonDrive
129+
d.MainShare = protonDrive.MainShare
130+
if d.RootFolderID == "root" || d.RootFolderID == "" {
131+
d.RootFolderID = protonDrive.RootLink.LinkID
132+
}
133+
d.MainShareKR = protonDrive.MainShareKR
134+
d.DefaultAddrKR = protonDrive.DefaultAddrKR
135+
136+
return nil
137+
}
138+
139+
func (d *ProtonDrive) Drop(ctx context.Context) error {
140+
return nil
141+
}
142+
143+
func (d *ProtonDrive) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
144+
entries, err := d.protonDrive.ListDirectory(ctx, dir.GetID())
145+
if err != nil {
146+
return nil, fmt.Errorf("failed to list directory: %w", err)
147+
}
148+
149+
objects := make([]model.Obj, 0, len(entries))
150+
for _, entry := range entries {
151+
obj := &model.Object{
152+
ID: entry.Link.LinkID,
153+
Name: entry.Name,
154+
Size: entry.Link.Size,
155+
Modified: time.Unix(entry.Link.ModifyTime, 0),
156+
IsFolder: entry.IsFolder,
157+
}
158+
objects = append(objects, obj)
159+
}
160+
161+
return objects, nil
162+
}
163+
164+
func (d *ProtonDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
165+
link, err := d.getLink(ctx, file.GetID())
166+
if err != nil {
167+
return nil, fmt.Errorf("failed get file link: %+v", err)
168+
}
169+
fileSystemAttrs, err := d.protonDrive.GetActiveRevisionAttrs(ctx, link)
170+
if err != nil {
171+
return nil, fmt.Errorf("failed get file revision: %+v", err)
172+
}
173+
// 解密后的文件大小
174+
size := fileSystemAttrs.Size
175+
176+
rangeReaderFunc := func(rangeCtx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
177+
length := httpRange.Length
178+
if length < 0 || httpRange.Start+length > size {
179+
length = size - httpRange.Start
180+
}
181+
reader, _, _, err := d.protonDrive.DownloadFile(rangeCtx, link, httpRange.Start)
182+
if err != nil {
183+
return nil, fmt.Errorf("failed start download: %+v", err)
184+
}
185+
return utils.ReadCloser{
186+
Reader: io.LimitReader(reader, length),
187+
Closer: reader,
188+
}, nil
189+
}
190+
191+
expiration := time.Minute
192+
return &model.Link{
193+
RangeReader: &model.FileRangeReader{
194+
RangeReaderIF: stream.RateLimitRangeReaderFunc(rangeReaderFunc),
195+
},
196+
ContentLength: size,
197+
Expiration: &expiration,
198+
}, nil
199+
}
200+
201+
func (d *ProtonDrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
202+
id, err := d.protonDrive.CreateNewFolderByID(ctx, parentDir.GetID(), dirName)
203+
if err != nil {
204+
return nil, fmt.Errorf("failed to create directory: %w", err)
205+
}
206+
207+
newDir := &model.Object{
208+
ID: id,
209+
Name: dirName,
210+
IsFolder: true,
211+
Modified: time.Now(),
212+
}
213+
return newDir, nil
214+
}
215+
216+
func (d *ProtonDrive) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
217+
return d.DirectMove(ctx, srcObj, dstDir)
218+
}
219+
220+
func (d *ProtonDrive) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
221+
if d.protonDrive == nil {
222+
return nil, fmt.Errorf("protonDrive bridge is nil")
223+
}
224+
225+
return d.DirectRename(ctx, srcObj, newName)
226+
}
227+
228+
func (d *ProtonDrive) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
229+
if srcObj.IsDir() {
230+
return nil, fmt.Errorf("directory copy not supported")
231+
}
232+
233+
srcLink, err := d.getLink(ctx, srcObj.GetID())
234+
if err != nil {
235+
return nil, err
236+
}
237+
238+
reader, linkSize, fileSystemAttrs, err := d.protonDrive.DownloadFile(ctx, srcLink, 0)
239+
if err != nil {
240+
return nil, fmt.Errorf("failed to download source file: %w", err)
241+
}
242+
defer reader.Close()
243+
244+
actualSize := linkSize
245+
if fileSystemAttrs != nil && fileSystemAttrs.Size > 0 {
246+
actualSize = fileSystemAttrs.Size
247+
}
248+
249+
file := &stream.FileStream{
250+
Ctx: ctx,
251+
Obj: &model.Object{
252+
Name: srcObj.GetName(),
253+
// Use the accurate and real size
254+
Size: actualSize,
255+
Modified: srcObj.ModTime(),
256+
},
257+
Reader: reader,
258+
}
259+
defer file.Close()
260+
return d.Put(ctx, dstDir, file, func(percentage float64) {})
261+
}
262+
263+
func (d *ProtonDrive) Remove(ctx context.Context, obj model.Obj) error {
264+
if obj.IsDir() {
265+
return d.protonDrive.MoveFolderToTrashByID(ctx, obj.GetID(), false)
266+
} else {
267+
return d.protonDrive.MoveFileToTrashByID(ctx, obj.GetID())
268+
}
269+
}
270+
271+
func (d *ProtonDrive) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
272+
return d.uploadFile(ctx, dstDir.GetID(), file, up)
273+
}
274+
275+
func (d *ProtonDrive) GetDetails(ctx context.Context) (*model.StorageDetails, error) {
276+
about, err := d.protonDrive.About(ctx)
277+
if err != nil {
278+
return nil, err
279+
}
280+
total := uint64(about.MaxSpace)
281+
free := total - uint64(about.UsedSpace)
282+
return &model.StorageDetails{
283+
DiskUsage: model.DiskUsage{
284+
TotalSpace: total,
285+
FreeSpace: free,
286+
},
287+
}, nil
288+
}
289+
290+
var _ driver.Driver = (*ProtonDrive)(nil)

drivers/proton_drive/meta.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package protondrive
2+
3+
/*
4+
Package protondrive
5+
Author: Da3zKi7<[email protected]>
6+
Date: 2025-09-18
7+
8+
Thanks to @henrybear327 for modded go-proton-api & Proton-API-Bridge
9+
10+
The power of open-source, the force of teamwork and the magic of reverse engineering!
11+
12+
13+
D@' 3z K!7 - The King Of Cracking
14+
15+
Да здравствует Родина))
16+
*/
17+
18+
import (
19+
"github.com/OpenListTeam/OpenList/v4/internal/driver"
20+
"github.com/OpenListTeam/OpenList/v4/internal/op"
21+
"github.com/henrybear327/Proton-API-Bridge/common"
22+
)
23+
24+
type Addition struct {
25+
driver.RootID
26+
Email string `json:"email" required:"true" type:"string"`
27+
Password string `json:"password" required:"true" type:"string"`
28+
TwoFACode string `json:"two_fa_code" type:"string"`
29+
ChunkSize int64 `json:"chunk_size" type:"number" default:"100"`
30+
UseReusableLogin bool `json:"use_reusable_login" type:"bool" default:"true" help:"Use reusable login credentials instead of username/password"`
31+
ReusableCredential common.ReusableCredentialData
32+
}
33+
34+
var config = driver.Config{
35+
Name: "ProtonDrive",
36+
LocalSort: true,
37+
OnlyProxy: true,
38+
DefaultRoot: "root",
39+
NoLinkURL: true,
40+
}
41+
42+
func init() {
43+
op.RegisterDriver(func() driver.Driver {
44+
return &ProtonDrive{
45+
Addition: Addition{
46+
UseReusableLogin: true,
47+
},
48+
apiBase: "https://drive.proton.me/api",
49+
appVersion: "[email protected]+rclone+proton",
50+
protonJson: "application/vnd.protonmail.v1+json",
51+
sdkVersion: "[email protected]",
52+
userAgent: "ProtonDrive/v1.70.0 (Windows NT 10.0.22000; Win64; x64)",
53+
webDriveAV: "[email protected]+0f69f7a8",
54+
}
55+
})
56+
}

0 commit comments

Comments
 (0)