-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
feat(drivers): add ProtonDrive driver #1368
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 eaa95ea
Update drivers/proton_drive/util.go
Da3zKi7 a209a85
chore
KirCute 517f4ee
Merge branch 'main' into feat/proton-drive
Da3zKi7 8caefc3
feat(drivers): enhance ProtonDrive temp server
Da3zKi7 68824b5
Merge branch 'main' into feat/proton-drive
Da3zKi7 32b745a
Merge branch 'main' into feat/proton-drive
Da3zKi7 f28cc4d
Merge branch 'main' into feat/proton-drive
Da3zKi7 ccb9298
Merge branch 'main' into feat/proton-drive
Da3zKi7 386961d
feat(proton_drive): refactor directory handling and improve link retr…
j2rong4cn 564d252
fix(proton_drive): add NoLinkURL configuration option
j2rong4cn 3d99787
fix(proton_drive): update file size retrieval and enforce TwoFACode r…
j2rong4cn 363624b
feat(proton_drive): add expiration to link response
j2rong4cn 9753fde
fix(proton_drive): handle empty RootFolderID in Init method
j2rong4cn e867092
fix(proton_drive): update credential handling to use email and reusab…
j2rong4cn 9d5a78a
fix(proton_drive): update credential handling to use reusableCredenti…
j2rong4cn 6254480
fix(proton_drive): update DirectRename to use GetLink for source obje…
j2rong4cn d7ce484
fix(proton_drive): refactor uploadFile to return model.Obj and handle…
j2rong4cn 493b96c
fix(proton_drive): refactor DirectMove to use getLink for source retr…
j2rong4cn 404879f
fix(proton_drive): simplify Copy method by removing temporary file cr…
j2rong4cn 2e36284
refactor(proton_drive): remove unused temporary server and related code
j2rong4cn f81cc1b
chore
KirCute 8868cf7
fix(proton_drive): fix driver
Da3zKi7 9341c8b
fix(proton_drive): simplify reusable login handling in Init method
j2rong4cn 7500971
Merge branch 'main' into feat/proton-drive
Da3zKi7 2fa6b05
fix(proton_drive): fix driver
Da3zKi7 ce45387
feat(proton_drive): improve authentication handling and remove unused…
j2rong4cn 855aab1
fix(proton_drive): fix driver
Da3zKi7 554dad0
fix(proton_drive): improve authentication handling
Da3zKi7 f29c58d
refactor(proton_drive): move client initialization to initClient method
j2rong4cn 0b84a1c
feat(proton_drive): move addrs and addrKRs
Da3zKi7 fdee792
feat(proton_drive): optimize upload threads
Da3zKi7 889077e
Merge branch 'main' into feat/proton-drive
Da3zKi7 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 == "" { | ||
KirCute marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
ILoveScratch2 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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", | ||
| } | ||
| }) | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.