From bc36afc823e273340a6a18e5b880d739822fab15 Mon Sep 17 00:00:00 2001 From: Da3zKi7 Date: Fri, 26 Sep 2025 12:03:44 -0600 Subject: [PATCH 01/26] 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. --- README.md | 1 + README_cn.md | 1 + README_ja.md | 1 + README_nl.md | 1 + drivers/all.go | 1 + drivers/proton_drive/driver.go | 420 +++++++++++++++ drivers/proton_drive/meta.go | 70 +++ drivers/proton_drive/types.go | 92 ++++ drivers/proton_drive/util.go | 943 +++++++++++++++++++++++++++++++++ go.mod | 19 + go.sum | 38 ++ 11 files changed, 1587 insertions(+) create mode 100644 drivers/proton_drive/driver.go create mode 100644 drivers/proton_drive/meta.go create mode 100644 drivers/proton_drive/types.go create mode 100644 drivers/proton_drive/util.go diff --git a/README.md b/README.md index 00f2b585d..c8337d14a 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ Thank you for your support and understanding of the OpenList project. - [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] [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) diff --git a/README_cn.md b/README_cn.md index fb6690507..e11a062fe 100644 --- a/README_cn.md +++ b/README_cn.md @@ -65,6 +65,7 @@ OpenList 是一个由 OpenList 团队独立维护的开源项目,遵循 AGPL-3 - [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV) - [x] Teambition([中国](https://www.teambition.com), [国际](https://us.teambition.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) diff --git a/README_ja.md b/README_ja.md index f39034288..5b343dbda 100644 --- a/README_ja.md +++ b/README_ja.md @@ -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) diff --git a/README_nl.md b/README_nl.md index 56260243a..8f9b63372 100644 --- a/README_nl.md +++ b/README_nl.md @@ -65,6 +65,7 @@ Dank u voor uw ondersteuning en begrip - [x] [WebDAV](https://en.wikipedia.org/wiki/WebDAV) - [x] Teambition([China](https://www.teambition.com), [Internationaal](https://us.teambition.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) diff --git a/drivers/all.go b/drivers/all.go index 197a936d0..14687e8a4 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -54,6 +54,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" diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go new file mode 100644 index 000000000..476270ab5 --- /dev/null +++ b/drivers/proton_drive/driver.go @@ -0,0 +1,420 @@ +package protondrive + +/* +Package protondrive +Author: Da3zKi7 +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" + "encoding/base64" + "fmt" + "net/http" + "sync" + "time" + + "github.com/OpenListTeam/OpenList/v4/internal/driver" + "github.com/OpenListTeam/OpenList/v4/internal/errs" + "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/internal/stream" + "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 + credentials *common.ProtonDriveCredential + + apiBase string + appVersion string + protonJson string + userAgent string + sdkVersion string + webDriveAV string + + tempServer *http.Server + tempServerPort int + downloadTokens map[string]*downloadInfo + tokenMutex sync.RWMutex + + c *proton.Client + //m *proton.Manager + + credentialCacheFile string + + //userKR *crypto.KeyRing + addrKRs map[string]*crypto.KeyRing + addrData map[string]proton.Address + + MainShare *proton.Share + RootLink *proton.Link + + 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) error { + + defer func() { + if r := recover(); r != nil { + fmt.Printf("ProtonDrive initialization panic: %v", r) + } + }() + + if d.Username == "" { + return fmt.Errorf("username is required") + } + if d.Password == "" { + return fmt.Errorf("password is required") + } + + //fmt.Printf("ProtonDrive Init: Username=%s, TwoFACode=%s", d.Username, d.TwoFACode) + + if ctx == nil { + return fmt.Errorf("context cannot be nil") + } + + cachedCredentials, err := d.loadCachedCredentials() + useReusableLogin := false + var reusableCredential *common.ReusableCredentialData + + if err == nil && cachedCredentials != nil && + cachedCredentials.UID != "" && cachedCredentials.AccessToken != "" && + cachedCredentials.RefreshToken != "" && cachedCredentials.SaltedKeyPass != "" { + useReusableLogin = true + reusableCredential = cachedCredentials + } else { + useReusableLogin = false + reusableCredential = &common.ReusableCredentialData{} + } + + config := &common.Config{ + AppVersion: d.appVersion, + UserAgent: d.userAgent, + FirstLoginCredential: &common.FirstLoginCredentialData{ + Username: d.Username, + Password: d.Password, + TwoFA: d.TwoFACode, + }, + EnableCaching: true, + ConcurrentBlockUploadCount: 5, + ConcurrentFileCryptoCount: 2, + UseReusableLogin: false, + ReplaceExistingDraft: true, + ReusableCredential: reusableCredential, + CredentialCacheFile: d.credentialCacheFile, + } + + if config.FirstLoginCredential == nil { + return fmt.Errorf("failed to create login credentials, FirstLoginCredential cannot be nil") + } + + //fmt.Printf("Calling NewProtonDrive...") + + protonDrive, credentials, err := proton_api_bridge.NewProtonDrive( + ctx, + config, + func(auth proton.Auth) {}, + func() {}, + ) + + if credentials == nil && !useReusableLogin { + return fmt.Errorf("failed to get credentials from NewProtonDrive") + } + + if err != nil { + return fmt.Errorf("failed to initialize ProtonDrive: %w", err) + } + + d.protonDrive = protonDrive + + var finalCredentials *common.ProtonDriveCredential + + if useReusableLogin { + + // For reusable login, create credentials from cached data + finalCredentials = &common.ProtonDriveCredential{ + UID: reusableCredential.UID, + AccessToken: reusableCredential.AccessToken, + RefreshToken: reusableCredential.RefreshToken, + SaltedKeyPass: reusableCredential.SaltedKeyPass, + } + + d.credentials = finalCredentials + } else { + d.credentials = credentials + } + + clientOptions := []proton.Option{ + proton.WithAppVersion(d.appVersion), + proton.WithUserAgent(d.userAgent), + } + manager := proton.New(clientOptions...) + d.c = manager.NewClient(d.credentials.UID, d.credentials.AccessToken, d.credentials.RefreshToken) + + saltedKeyPassBytes, err := base64.StdEncoding.DecodeString(d.credentials.SaltedKeyPass) + if err != nil { + return fmt.Errorf("failed to decode salted key pass: %w", err) + } + + _, addrKRs, addrs, _, err := getAccountKRs(ctx, d.c, nil, saltedKeyPassBytes) + if err != nil { + return fmt.Errorf("failed to get account keyrings: %w", err) + } + + d.MainShare = protonDrive.MainShare + d.RootLink = protonDrive.RootLink + d.MainShareKR = protonDrive.MainShareKR + d.DefaultAddrKR = protonDrive.DefaultAddrKR + d.addrKRs = addrKRs + d.addrData = addrs + + return nil +} + +func (d *ProtonDrive) Drop(ctx context.Context) error { + if d.tempServer != nil { + d.tempServer.Shutdown(ctx) + } + return nil +} + +func (d *ProtonDrive) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { + var linkID string + + if dir.GetPath() == "/" { + linkID = d.protonDrive.RootLink.LinkID + } else { + + link, err := d.searchByPath(ctx, dir.GetPath(), true) + if err != nil { + return nil, err + } + linkID = link.LinkID + } + + entries, err := d.protonDrive.ListDirectory(ctx, linkID) + if err != nil { + return nil, fmt.Errorf("failed to list directory: %w", err) + } + + //fmt.Printf("Found %d entries for path %s\n", len(entries), dir.GetPath()) + //fmt.Printf("Found %d entries\n", len(entries)) + + if len(entries) == 0 { + emptySlice := []model.Obj{} + + //fmt.Printf("Returning empty slice (entries): %+v\n", emptySlice) + + return emptySlice, nil + } + + var objects []model.Obj + for _, entry := range entries { + obj := &model.Object{ + 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.searchByPath(ctx, file.GetPath(), false) + if err != nil { + return nil, err + } + + if err := d.ensureTempServer(); err != nil { + return nil, fmt.Errorf("failed to start temp server: %w", err) + } + + token := d.generateDownloadToken(link.LinkID, file.GetName()) + + /* return &model.Link{ + URL: fmt.Sprintf("protondrive://download/%s", link.LinkID), + }, nil */ + + return &model.Link{ + URL: fmt.Sprintf("http://localhost:%d/temp/%s", d.tempServerPort, token), + }, nil +} + +func (d *ProtonDrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) { + var parentLinkID string + + if parentDir.GetPath() == "/" { + parentLinkID = d.protonDrive.RootLink.LinkID + } else { + link, err := d.searchByPath(ctx, parentDir.GetPath(), true) + if err != nil { + return nil, err + } + parentLinkID = link.LinkID + } + + _, err := d.protonDrive.CreateNewFolderByID(ctx, parentLinkID, dirName) + if err != nil { + return nil, fmt.Errorf("failed to create directory: %w", err) + } + + newDir := &model.Object{ + 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.searchByPath(ctx, srcObj.GetPath(), false) + 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 + } + + tempFile, err := utils.CreateTempFile(reader, actualSize) + if err != nil { + return nil, fmt.Errorf("failed to create temp file: %w", err) + } + defer tempFile.Close() + + updatedObj := &model.Object{ + Name: srcObj.GetName(), + // Use the accurate and real size + Size: actualSize, + Modified: srcObj.ModTime(), + IsFolder: false, + } + + return d.Put(ctx, dstDir, &stream.FileStream{ + Ctx: ctx, + Obj: updatedObj, + Reader: tempFile, + }, nil) +} + +func (d *ProtonDrive) Remove(ctx context.Context, obj model.Obj) error { + link, err := d.searchByPath(ctx, obj.GetPath(), obj.IsDir()) + if err != nil { + return err + } + + if obj.IsDir() { + return d.protonDrive.MoveFolderToTrashByID(ctx, link.LinkID, false) + } else { + return d.protonDrive.MoveFileToTrashByID(ctx, link.LinkID) + } +} + +func (d *ProtonDrive) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { + var parentLinkID string + + if dstDir.GetPath() == "/" { + parentLinkID = d.protonDrive.RootLink.LinkID + } else { + link, err := d.searchByPath(ctx, dstDir.GetPath(), true) + if err != nil { + return nil, err + } + parentLinkID = link.LinkID + } + + tempFile, err := utils.CreateTempFile(file, file.GetSize()) + if err != nil { + return nil, fmt.Errorf("failed to create temp file: %w", err) + } + defer tempFile.Close() + + err = d.uploadFile(ctx, parentLinkID, file.GetName(), tempFile, file.GetSize(), up) + if err != nil { + return nil, err + } + + uploadedObj := &model.Object{ + Name: file.GetName(), + Size: file.GetSize(), + Modified: file.ModTime(), + IsFolder: false, + } + return uploadedObj, nil +} + +func (d *ProtonDrive) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) { + // TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional + return nil, errs.NotImplement +} + +func (d *ProtonDrive) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) { + // TODO list args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional + return nil, errs.NotImplement +} + +func (d *ProtonDrive) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) { + // TODO return link of file args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional + return nil, errs.NotImplement +} + +func (d *ProtonDrive) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) { + // TODO extract args.InnerPath path in the archive srcObj to the dstDir location, optional + // a folder with the same name as the archive file needs to be created to store the extracted results if args.PutIntoNewDir + // return errs.NotImplement to use an internal archive tool + return nil, errs.NotImplement +} + +var _ driver.Driver = (*ProtonDrive)(nil) diff --git a/drivers/proton_drive/meta.go b/drivers/proton_drive/meta.go new file mode 100644 index 000000000..89e485df3 --- /dev/null +++ b/drivers/proton_drive/meta.go @@ -0,0 +1,70 @@ +package protondrive + +/* +Package protondrive +Author: Da3zKi7 +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" +) + +type Addition struct { + driver.RootPath + //driver.RootID + + Username string `json:"username" required:"true" type:"string"` + Password string `json:"password" required:"true" type:"string"` + TwoFACode string `json:"two_fa_code,omitempty" type:"string"` + ChunkSize int64 `json:"chunk_size" type:"number" default:"100"` +} + +type Config struct { + Name string `json:"name"` + LocalSort bool `json:"local_sort"` + OnlyLocal bool `json:"only_local"` + OnlyProxy bool `json:"only_proxy"` + NoCache bool `json:"no_cache"` + NoUpload bool `json:"no_upload"` + NeedMs bool `json:"need_ms"` + DefaultRoot string `json:"default_root"` +} + +var config = driver.Config{ + Name: "ProtonDrive", + LocalSort: false, + //OnlyLocal: false, + OnlyProxy: false, + NoCache: false, + NoUpload: false, + NeedMs: false, + DefaultRoot: "/", + CheckStatus: false, + Alert: "", + NoOverwriteUpload: false, +} + +func init() { + op.RegisterDriver(func() driver.Driver { + return &ProtonDrive{ + apiBase: "https://drive.proton.me/api", + appVersion: "windows-drive@1.11.3+rclone+proton", + credentialCacheFile: "./data/.prtcrd", + 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", + } + }) +} diff --git a/drivers/proton_drive/types.go b/drivers/proton_drive/types.go new file mode 100644 index 000000000..17e47172a --- /dev/null +++ b/drivers/proton_drive/types.go @@ -0,0 +1,92 @@ +package protondrive + +/* +Package protondrive +Author: Da3zKi7 +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 ( + "io" + "time" + + "github.com/OpenListTeam/OpenList/v4/internal/driver" + "github.com/henrybear327/go-proton-api" +) + +type ProtonFile struct { + *proton.Link + Name string + IsFolder bool +} + +func (p *ProtonFile) GetName() string { + return p.Name +} + +func (p *ProtonFile) GetSize() int64 { + return p.Link.Size +} + +func (p *ProtonFile) GetPath() string { + return p.Name +} + +func (p *ProtonFile) IsDir() bool { + return p.IsFolder +} + +func (p *ProtonFile) ModTime() time.Time { + return time.Unix(p.Link.ModifyTime, 0) +} + +func (p *ProtonFile) CreateTime() time.Time { + return time.Unix(p.Link.CreateTime, 0) +} + +type downloadInfo struct { + LinkID string + FileName string +} + +type httpRange struct { + start, end int64 +} + +type MoveRequest struct { + ParentLinkID string `json:"ParentLinkID"` + NodePassphrase string `json:"NodePassphrase"` + NodePassphraseSignature *string `json:"NodePassphraseSignature"` + Name string `json:"Name"` + NameSignatureEmail string `json:"NameSignatureEmail"` + Hash string `json:"Hash"` + OriginalHash string `json:"OriginalHash"` + ContentHash *string `json:"ContentHash"` // Maybe null +} + +type progressReader struct { + reader io.Reader + total int64 + current int64 + callback driver.UpdateProgress +} + +type RenameRequest struct { + Name string `json:"Name"` // PGP encrypted name + NameSignatureEmail string `json:"NameSignatureEmail"` // User's signature email + Hash string `json:"Hash"` // New name hash + OriginalHash string `json:"OriginalHash"` // Current name hash +} + +type RenameResponse struct { + Code int `json:"Code"` +} diff --git a/drivers/proton_drive/util.go b/drivers/proton_drive/util.go new file mode 100644 index 000000000..67c9de633 --- /dev/null +++ b/drivers/proton_drive/util.go @@ -0,0 +1,943 @@ +package protondrive + +/* +Package protondrive +Author: Da3zKi7 +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 ( + "bufio" + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "mime" + "net" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/OpenListTeam/OpenList/v4/internal/driver" + "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/ProtonMail/gopenpgp/v2/crypto" + "github.com/henrybear327/Proton-API-Bridge/common" + "github.com/henrybear327/go-proton-api" +) + +func (d *ProtonDrive) loadCachedCredentials() (*common.ReusableCredentialData, error) { + if d.credentialCacheFile == "" { + return nil, nil + } + + if _, err := os.Stat(d.credentialCacheFile); os.IsNotExist(err) { + return nil, nil + } + + data, err := os.ReadFile(d.credentialCacheFile) + if err != nil { + return nil, fmt.Errorf("failed to read credential cache file: %w", err) + } + + var credentials common.ReusableCredentialData + if err := json.Unmarshal(data, &credentials); err != nil { + return nil, fmt.Errorf("failed to parse cached credentials: %w", err) + } + + if credentials.UID == "" || credentials.AccessToken == "" || + credentials.RefreshToken == "" || credentials.SaltedKeyPass == "" { + return nil, fmt.Errorf("cached credentials are incomplete") + } + + return &credentials, nil +} + +func (d *ProtonDrive) searchByPath(ctx context.Context, fullPath string, isFolder bool) (*proton.Link, error) { + if fullPath == "/" { + return d.protonDrive.RootLink, nil + } + + cleanPath := strings.Trim(fullPath, "/") + pathParts := strings.Split(cleanPath, "/") + + currentLink := d.protonDrive.RootLink + + for i, part := range pathParts { + isLastPart := i == len(pathParts)-1 + searchForFolder := !isLastPart || isFolder + + entries, err := d.protonDrive.ListDirectory(ctx, currentLink.LinkID) + if err != nil { + return nil, fmt.Errorf("failed to list directory: %w", err) + + } + + found := false + for _, entry := range entries { + // entry.Name is already decrypted! + if entry.Name == part && entry.IsFolder == searchForFolder { + currentLink = entry.Link + found = true + break + } + } + + if !found { + return nil, fmt.Errorf("path not found: %s (looking for part: %s)", fullPath, part) + } + } + + return currentLink, nil +} + +func (pr *progressReader) Read(p []byte) (int, error) { + n, err := pr.reader.Read(p) + pr.current += int64(n) + + if pr.callback != nil { + percentage := float64(pr.current) / float64(pr.total) * 100 + pr.callback(percentage) + } + + return n, err +} + +func (d *ProtonDrive) uploadFile(ctx context.Context, parentLinkID, fileName string, file *os.File, size int64, up driver.UpdateProgress) error { + + fileInfo, err := file.Stat() + if err != nil { + return fmt.Errorf("failed to get file info: %w", err) + } + + _, err = d.protonDrive.GetLink(ctx, parentLinkID) + if err != nil { + return fmt.Errorf("failed to get parent link: %w", err) + } + + var reader io.Reader + + // Use buffered reader with larger buffer for better performance + var bufferSize int + + // File > 100MB (default) + if size > d.ChunkSize*1024*1024 { + + // 256KB for large files + bufferSize = 256 * 1024 + + // File > 10MB + } else if size > 10*1024*1024 { + + // 128KB for medium files + bufferSize = 128 * 1024 + } else { + + // 64KB for small files + bufferSize = 64 * 1024 + } + + //reader = bufio.NewReader(file) + reader = bufio.NewReaderSize(file, bufferSize) + + reader = &progressReader{ + reader: reader, + total: size, + current: 0, + callback: up, + } + + _, _, err = d.protonDrive.UploadFileByReader(ctx, parentLinkID, fileName, fileInfo.ModTime(), reader, 0) + if err != nil { + return fmt.Errorf("failed to upload file: %w", err) + } + + return nil +} + +func (d *ProtonDrive) ensureTempServer() error { + if d.tempServer != nil { + + // Already running + return nil + } + + listener, err := net.Listen("tcp", ":0") + if err != nil { + return err + } + d.tempServerPort = listener.Addr().(*net.TCPAddr).Port + + mux := http.NewServeMux() + mux.HandleFunc("/temp/", d.handleTempDownload) + + d.tempServer = &http.Server{ + Handler: mux, + } + + go func() { + d.tempServer.Serve(listener) + }() + + return nil +} + +func (d *ProtonDrive) handleTempDownload(w http.ResponseWriter, r *http.Request) { + token := strings.TrimPrefix(r.URL.Path, "/temp/") + + d.tokenMutex.RLock() + info, exists := d.downloadTokens[token] + d.tokenMutex.RUnlock() + + if !exists { + http.Error(w, "Invalid or expired token", http.StatusNotFound) + return + } + + link, err := d.protonDrive.GetLink(r.Context(), info.LinkID) + if err != nil { + http.Error(w, "Failed to get file link", http.StatusInternalServerError) + return + } + + // Get file size for range calculations + _, _, attrs, err := d.protonDrive.DownloadFile(r.Context(), link, 0) + if err != nil { + http.Error(w, "Failed to get file info", http.StatusInternalServerError) + return + } + + fileSize := attrs.Size + + rangeHeader := r.Header.Get("Range") + if rangeHeader != "" { + + // Parse range header like "bytes=0-1023" or "bytes=1024-" + ranges, err := parseRange(rangeHeader, fileSize) + if err != nil { + http.Error(w, "Invalid range", http.StatusRequestedRangeNotSatisfiable) + return + } + + if len(ranges) == 1 { + + // Single range request, small + start, end := ranges[0].start, ranges[0].end + contentLength := end - start + 1 + + // Start download from offset + reader, _, _, err := d.protonDrive.DownloadFile(r.Context(), link, start) + if err != nil { + http.Error(w, "Failed to start download", http.StatusInternalServerError) + return + } + defer reader.Close() + + w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fileSize)) + w.Header().Set("Content-Length", fmt.Sprintf("%d", contentLength)) + w.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(link.Name))) + + // Partial content... + // Setting fileName is more cosmetical here + //.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", link.Name)) + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", info.FileName)) + w.Header().Set("Accept-Ranges", "bytes") + + w.WriteHeader(http.StatusPartialContent) + + io.CopyN(w, reader, contentLength) + return + } + } + + // Full file download (non-range request) + reader, _, _, err := d.protonDrive.DownloadFile(r.Context(), link, 0) + if err != nil { + http.Error(w, "Failed to start download", http.StatusInternalServerError) + return + } + defer reader.Close() + + // Set headers for full content + w.Header().Set("Content-Length", fmt.Sprintf("%d", fileSize)) + w.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(link.Name))) + + // Setting fileName is needed since ProtonDrive fileName is more like a random string + //w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", link.Name)) + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", info.FileName)) + + w.Header().Set("Accept-Ranges", "bytes") + + // Stream the full file + io.Copy(w, reader) +} + +func (d *ProtonDrive) generateDownloadToken(linkID, fileName string) string { + token := fmt.Sprintf("%d_%s", time.Now().UnixNano(), linkID[:8]) + + d.tokenMutex.Lock() + if d.downloadTokens == nil { + d.downloadTokens = make(map[string]*downloadInfo) + } + + d.downloadTokens[token] = &downloadInfo{ + LinkID: linkID, + FileName: fileName, + } + + d.tokenMutex.Unlock() + + go func() { + + // Token expires in 1 hour + time.Sleep(1 * time.Hour) + d.tokenMutex.Lock() + + delete(d.downloadTokens, token) + d.tokenMutex.Unlock() + }() + + return token +} + +func parseRange(rangeHeader string, size int64) ([]httpRange, error) { + if !strings.HasPrefix(rangeHeader, "bytes=") { + return nil, fmt.Errorf("invalid range header") + } + + rangeSpec := strings.TrimPrefix(rangeHeader, "bytes=") + ranges := strings.Split(rangeSpec, ",") + + var result []httpRange + for _, r := range ranges { + r = strings.TrimSpace(r) + if strings.Contains(r, "-") { + parts := strings.Split(r, "-") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid range format") + } + + var start, end int64 + var err error + + if parts[0] == "" { + + // Suffix range (e.g., "-500") + if parts[1] == "" { + return nil, fmt.Errorf("invalid range format") + } + end = size - 1 + start, err = strconv.ParseInt(parts[1], 10, 64) + if err != nil { + return nil, err + } + start = size - start + if start < 0 { + start = 0 + } + } else if parts[1] == "" { + + // Prefix range (e.g., "500-") + start, err = strconv.ParseInt(parts[0], 10, 64) + if err != nil { + return nil, err + } + end = size - 1 + } else { + // Full range (e.g., "0-1023") + start, err = strconv.ParseInt(parts[0], 10, 64) + if err != nil { + return nil, err + } + end, err = strconv.ParseInt(parts[1], 10, 64) + if err != nil { + return nil, err + } + } + + if start >= size || end >= size || start > end { + return nil, fmt.Errorf("range out of bounds") + } + + result = append(result, httpRange{start: start, end: end}) + } + } + + return result, nil +} + +func (d *ProtonDrive) encryptFileName(ctx context.Context, name string, parentLinkID string) (string, error) { + + parentLink, err := d.getLink(ctx, parentLinkID) + if err != nil { + return "", fmt.Errorf("failed to get parent link: %w", err) + } + + // Get parent node keyring + parentNodeKR, err := d.getLinkKR(ctx, parentLink) + if err != nil { + return "", fmt.Errorf("failed to get parent keyring: %w", err) + } + + // Temporary file (request) + tempReq := proton.CreateFileReq{ + SignatureAddress: d.MainShare.Creator, + } + + // Encrypt the filename + err = tempReq.SetName(name, d.DefaultAddrKR, parentNodeKR) + if err != nil { + return "", fmt.Errorf("failed to encrypt filename: %w", err) + } + + return tempReq.Name, nil +} + +func (d *ProtonDrive) generateFileNameHash(ctx context.Context, name string, parentLinkID string) (string, error) { + + parentLink, err := d.getLink(ctx, parentLinkID) + if err != nil { + return "", fmt.Errorf("failed to get parent link: %w", err) + } + + // Get parent node keyring + parentNodeKR, err := d.getLinkKR(ctx, parentLink) + if err != nil { + return "", fmt.Errorf("failed to get parent keyring: %w", err) + } + + signatureVerificationKR, err := d.getSignatureVerificationKeyring([]string{parentLink.SignatureEmail}, parentNodeKR) + if err != nil { + return "", fmt.Errorf("failed to get signature verification keyring: %w", err) + } + + parentHashKey, err := parentLink.GetHashKey(parentNodeKR, signatureVerificationKR) + if err != nil { + return "", fmt.Errorf("failed to get parent hash key: %w", err) + } + + nameHash, err := proton.GetNameHash(name, parentHashKey) + if err != nil { + return "", fmt.Errorf("failed to generate name hash: %w", err) + } + + return nameHash, nil +} + +func (d *ProtonDrive) getOriginalNameHash(link *proton.Link) (string, error) { + if link == nil { + return "", fmt.Errorf("link cannot be nil") + } + + if link.Hash == "" { + return "", fmt.Errorf("link hash is empty") + } + + return link.Hash, nil +} + +func (d *ProtonDrive) getLink(ctx context.Context, linkID string) (*proton.Link, error) { + if linkID == "" { + return nil, fmt.Errorf("linkID cannot be empty") + } + + link, err := d.c.GetLink(ctx, d.MainShare.ShareID, linkID) + if err != nil { + return nil, err + } + + return &link, nil +} + +func (d *ProtonDrive) getLinkKR(ctx context.Context, link *proton.Link) (*crypto.KeyRing, error) { + if link == nil { + return nil, fmt.Errorf("link cannot be nil") + } + + // Root Link or Root Dir + if link.ParentLinkID == "" { + signatureVerificationKR, err := d.getSignatureVerificationKeyring([]string{link.SignatureEmail}) + if err != nil { + return nil, err + } + return link.GetKeyRing(d.MainShareKR, signatureVerificationKR) + } + + // Get parent keyring recursively + parentLink, err := d.getLink(ctx, link.ParentLinkID) + if err != nil { + return nil, err + } + + parentNodeKR, err := d.getLinkKR(ctx, parentLink) + if err != nil { + return nil, err + } + + signatureVerificationKR, err := d.getSignatureVerificationKeyring([]string{link.SignatureEmail}) + if err != nil { + return nil, err + } + + return link.GetKeyRing(parentNodeKR, signatureVerificationKR) +} + +var ( + ErrKeyPassOrSaltedKeyPassMustBeNotNil = errors.New("either keyPass or saltedKeyPass must be not nil") + ErrFailedToUnlockUserKeys = errors.New("failed to unlock user keys") +) + +func getAccountKRs(ctx context.Context, c *proton.Client, keyPass, saltedKeyPass []byte) (*crypto.KeyRing, map[string]*crypto.KeyRing, map[string]proton.Address, []byte, error) { + + user, err := c.GetUser(ctx) + if err != nil { + return nil, nil, nil, nil, err + } + // fmt.Printf("user %#v", user) + + addrsArr, err := c.GetAddresses(ctx) + if err != nil { + return nil, nil, nil, nil, err + } + // fmt.Printf("addr %#v", addr) + + if saltedKeyPass == nil { + if keyPass == nil { + return nil, nil, nil, nil, ErrKeyPassOrSaltedKeyPassMustBeNotNil + } + + // Due to limitations, salts are stored using cacheCredentialToFile + salts, err := c.GetSalts(ctx) + if err != nil { + return nil, nil, nil, nil, err + } + // fmt.Printf("salts %#v", salts) + + saltedKeyPass, err = salts.SaltForKey(keyPass, user.Keys.Primary().ID) + if err != nil { + return nil, nil, nil, nil, err + } + // fmt.Printf("saltedKeyPass ok") + } + + userKR, addrKRs, err := proton.Unlock(user, addrsArr, saltedKeyPass, nil) + if err != nil { + return nil, nil, nil, nil, err + + } else if userKR.CountDecryptionEntities() == 0 { + return nil, nil, nil, nil, ErrFailedToUnlockUserKeys + } + + addrs := make(map[string]proton.Address) + for _, addr := range addrsArr { + addrs[addr.Email] = addr + } + + return userKR, addrKRs, addrs, saltedKeyPass, nil +} + +func (d *ProtonDrive) getSignatureVerificationKeyring(emailAddresses []string, verificationAddrKRs ...*crypto.KeyRing) (*crypto.KeyRing, error) { + ret, err := crypto.NewKeyRing(nil) + if err != nil { + return nil, err + } + + for _, emailAddress := range emailAddresses { + if addr, ok := d.addrData[emailAddress]; ok { + if addrKR, exists := d.addrKRs[addr.ID]; exists { + err = d.addKeysFromKR(ret, addrKR) + if err != nil { + return nil, err + } + } + } + } + + for _, kr := range verificationAddrKRs { + err = d.addKeysFromKR(ret, kr) + if err != nil { + return nil, err + } + } + + if ret.CountEntities() == 0 { + return nil, fmt.Errorf("no keyring for signature verification") + } + + return ret, nil +} + +func (d *ProtonDrive) addKeysFromKR(kr *crypto.KeyRing, newKRs ...*crypto.KeyRing) error { + for i := range newKRs { + for _, key := range newKRs[i].GetKeys() { + err := kr.AddKey(key) + if err != nil { + return err + } + } + } + return nil +} + +func (d *ProtonDrive) DirectRename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) { + //fmt.Printf("DEBUG DirectRename: path=%s, newName=%s", srcObj.GetPath(), newName) + + if d.MainShare == nil || d.DefaultAddrKR == nil { + return nil, fmt.Errorf("missing required fields: MainShare=%v, DefaultAddrKR=%v", + d.MainShare != nil, d.DefaultAddrKR != nil) + } + + if d.protonDrive == nil { + return nil, fmt.Errorf("protonDrive bridge is nil") + } + + srcLink, err := d.searchByPath(ctx, srcObj.GetPath(), srcObj.IsDir()) + if err != nil { + return nil, fmt.Errorf("failed to find source: %w", err) + } + + parentLinkID := srcLink.ParentLinkID + if parentLinkID == "" { + return nil, fmt.Errorf("cannot rename root folder") + } + + encryptedName, err := d.encryptFileName(ctx, newName, parentLinkID) + if err != nil { + return nil, fmt.Errorf("failed to encrypt filename: %w", err) + } + + newHash, err := d.generateFileNameHash(ctx, newName, parentLinkID) + if err != nil { + return nil, fmt.Errorf("failed to generate new hash: %w", err) + } + + originalHash, err := d.getOriginalNameHash(srcLink) + if err != nil { + return nil, fmt.Errorf("failed to get original hash: %w", err) + } + + renameReq := RenameRequest{ + Name: encryptedName, + NameSignatureEmail: d.MainShare.Creator, + Hash: newHash, + OriginalHash: originalHash, + } + + err = d.executeRenameAPI(ctx, srcLink.LinkID, renameReq) + if err != nil { + return nil, fmt.Errorf("rename API call failed: %w", err) + } + + return &model.Object{ + Name: newName, + Size: srcObj.GetSize(), + Modified: srcObj.ModTime(), + IsFolder: srcObj.IsDir(), + }, nil +} + +func (d *ProtonDrive) executeRenameAPI(ctx context.Context, linkID string, req RenameRequest) error { + + renameURL := fmt.Sprintf(d.apiBase+"/drive/v2/volumes/%s/links/%s/rename", + d.MainShare.VolumeID, linkID) + + reqBody, err := json.Marshal(req) + if err != nil { + return fmt.Errorf("failed to marshal rename request: %w", err) + } + + httpReq, err := http.NewRequestWithContext(ctx, "PUT", renameURL, bytes.NewReader(reqBody)) + if err != nil { + return fmt.Errorf("failed to create HTTP request: %w", err) + } + + httpReq.Header.Set("Content-Type", "application/json") + httpReq.Header.Set("Accept", d.protonJson) + httpReq.Header.Set("X-Pm-Appversion", d.webDriveAV) + httpReq.Header.Set("X-Pm-Drive-Sdk-Version", d.sdkVersion) + httpReq.Header.Set("X-Pm-Uid", d.credentials.UID) + httpReq.Header.Set("Authorization", "Bearer "+d.credentials.AccessToken) + + client := &http.Client{} + resp, err := client.Do(httpReq) + if err != nil { + return fmt.Errorf("failed to execute rename request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("rename failed with status %d", resp.StatusCode) + } + + var renameResp RenameResponse + if err := json.NewDecoder(resp.Body).Decode(&renameResp); err != nil { + return fmt.Errorf("failed to decode rename response: %w", err) + } + + if renameResp.Code != 1000 { + return fmt.Errorf("rename failed with code %d", renameResp.Code) + } + + return nil +} + +func (d *ProtonDrive) executeMoveAPI(ctx context.Context, linkID string, req MoveRequest) error { + //fmt.Printf("DEBUG Move Request - Name: %s\n", req.Name) + //fmt.Printf("DEBUG Move Request - Hash: %s\n", req.Hash) + //fmt.Printf("DEBUG Move Request - OriginalHash: %s\n", req.OriginalHash) + //fmt.Printf("DEBUG Move Request - ParentLinkID: %s\n", req.ParentLinkID) + + //fmt.Printf("DEBUG Move Request - Name length: %d\n", len(req.Name)) + //fmt.Printf("DEBUG Move Request - NameSignatureEmail: %s\n", req.NameSignatureEmail) + //fmt.Printf("DEBUG Move Request - ContentHash: %v\n", req.ContentHash) + //fmt.Printf("DEBUG Move Request - NodePassphrase length: %d\n", len(req.NodePassphrase)) + //fmt.Printf("DEBUG Move Request - NodePassphraseSignature length: %d\n", len(req.NodePassphraseSignature)) + + //fmt.Printf("DEBUG Move Request - SrcLinkID: %s\n", linkID) + //fmt.Printf("DEBUG Move Request - DstParentLinkID: %s\n", req.ParentLinkID) + //fmt.Printf("DEBUG Move Request - ShareID: %s\n", d.MainShare.ShareID) + + srcLink, _ := d.getLink(ctx, linkID) + if srcLink != nil && srcLink.ParentLinkID == req.ParentLinkID { + return fmt.Errorf("cannot move to same parent directory") + } + + moveURL := fmt.Sprintf(d.apiBase+"/drive/v2/volumes/%s/links/%s/move", + d.MainShare.VolumeID, linkID) + + reqBody, err := json.Marshal(req) + if err != nil { + return fmt.Errorf("failed to marshal move request: %w", err) + } + + httpReq, err := http.NewRequestWithContext(ctx, "PUT", moveURL, bytes.NewReader(reqBody)) + if err != nil { + return fmt.Errorf("failed to create HTTP request: %w", err) + } + + httpReq.Header.Set("Authorization", "Bearer "+d.credentials.AccessToken) + httpReq.Header.Set("Accept", d.protonJson) + httpReq.Header.Set("X-Pm-Appversion", d.webDriveAV) + httpReq.Header.Set("X-Pm-Drive-Sdk-Version", d.sdkVersion) + httpReq.Header.Set("X-Pm-Uid", d.credentials.UID) + httpReq.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(httpReq) + if err != nil { + return fmt.Errorf("failed to execute move request: %w", err) + } + defer resp.Body.Close() + + var moveResp RenameResponse + if err := json.NewDecoder(resp.Body).Decode(&moveResp); err != nil { + return fmt.Errorf("failed to decode move response: %w", err) + } + + if moveResp.Code != 1000 { + return fmt.Errorf("move operation failed with code: %d", moveResp.Code) + } + + return nil +} + +func (d *ProtonDrive) DirectMove(ctx context.Context, srcObj model.Obj, dstDir model.Obj) (model.Obj, error) { + //fmt.Printf("DEBUG DirectMove: srcPath=%s, dstPath=%s", srcObj.GetPath(), dstDir.GetPath()) + + srcLink, err := d.searchByPath(ctx, srcObj.GetPath(), srcObj.IsDir()) + if err != nil { + return nil, fmt.Errorf("failed to find source: %w", err) + } + + var dstParentLinkID string + if dstDir.GetPath() == "/" { + dstParentLinkID = d.RootLink.LinkID + } else { + dstLink, err := d.searchByPath(ctx, dstDir.GetPath(), true) + if err != nil { + return nil, fmt.Errorf("failed to find destination: %w", err) + } + dstParentLinkID = dstLink.LinkID + } + + if srcObj.IsDir() { + + // Check if destination is a descendant of source + if err := d.checkCircularMove(ctx, srcLink.LinkID, dstParentLinkID); err != nil { + return nil, err + } + } + + // Encrypt the filename for the new location + encryptedName, err := d.encryptFileName(ctx, srcObj.GetName(), dstParentLinkID) + if err != nil { + return nil, fmt.Errorf("failed to encrypt filename: %w", err) + } + + newHash, err := d.generateNameHash(ctx, srcObj.GetName(), dstParentLinkID) + if err != nil { + return nil, fmt.Errorf("failed to generate new hash: %w", err) + } + + originalHash, err := d.getOriginalNameHash(srcLink) + if err != nil { + return nil, fmt.Errorf("failed to get original hash: %w", err) + } + + // Re-encrypt node passphrase for new parent context + reencryptedPassphrase, err := d.reencryptNodePassphrase(ctx, srcLink, dstParentLinkID) + if err != nil { + return nil, fmt.Errorf("failed to re-encrypt node passphrase: %w", err) + } + + moveReq := MoveRequest{ + ParentLinkID: dstParentLinkID, + NodePassphrase: reencryptedPassphrase, + Name: encryptedName, + NameSignatureEmail: d.MainShare.Creator, + Hash: newHash, + OriginalHash: originalHash, + ContentHash: nil, + + // *** Causes rejection *** + /* NodePassphraseSignature: srcLink.NodePassphraseSignature, */ + } + + //fmt.Printf("DEBUG MoveRequest validation:\n") + //fmt.Printf(" Name length: %d\n", len(moveReq.Name)) + //fmt.Printf(" Hash: %s\n", moveReq.Hash) + //fmt.Printf(" OriginalHash: %s\n", moveReq.OriginalHash) + //fmt.Printf(" NodePassphrase length: %d\n", len(moveReq.NodePassphrase)) + /* fmt.Printf(" NodePassphraseSignature length: %d\n", len(moveReq.NodePassphraseSignature)) */ + //fmt.Printf(" NameSignatureEmail: %s\n", moveReq.NameSignatureEmail) + + err = d.executeMoveAPI(ctx, srcLink.LinkID, moveReq) + if err != nil { + return nil, fmt.Errorf("move API call failed: %w", err) + } + + return &model.Object{ + Name: srcObj.GetName(), + Size: srcObj.GetSize(), + Modified: srcObj.ModTime(), + IsFolder: srcObj.IsDir(), + }, nil +} + +func (d *ProtonDrive) reencryptNodePassphrase(ctx context.Context, srcLink *proton.Link, dstParentLinkID string) (string, error) { + // Get source parent link with metadata + srcParentLink, err := d.getLink(ctx, srcLink.ParentLinkID) + if err != nil { + return "", fmt.Errorf("failed to get source parent link: %w", err) + } + + // Get source parent keyring using link object + srcParentKR, err := d.getLinkKR(ctx, srcParentLink) + if err != nil { + return "", fmt.Errorf("failed to get source parent keyring: %w", err) + } + + // Get destination parent link with metadata + dstParentLink, err := d.getLink(ctx, dstParentLinkID) + if err != nil { + return "", fmt.Errorf("failed to get destination parent link: %w", err) + } + + // Get destination parent keyring using link object + dstParentKR, err := d.getLinkKR(ctx, dstParentLink) + if err != nil { + return "", fmt.Errorf("failed to get destination parent keyring: %w", err) + } + + // Re-encrypt the node passphrase from source parent context to destination parent context + reencryptedPassphrase, err := reencryptKeyPacket(srcParentKR, dstParentKR, d.DefaultAddrKR, srcLink.NodePassphrase) + if err != nil { + return "", fmt.Errorf("failed to re-encrypt key packet: %w", err) + } + + return reencryptedPassphrase, nil +} + +func (d *ProtonDrive) generateNameHash(ctx context.Context, name string, parentLinkID string) (string, error) { + + parentLink, err := d.getLink(ctx, parentLinkID) + if err != nil { + return "", fmt.Errorf("failed to get parent link: %w", err) + } + + // Get parent node keyring + parentNodeKR, err := d.getLinkKR(ctx, parentLink) + if err != nil { + return "", fmt.Errorf("failed to get parent keyring: %w", err) + } + + // Get signature verification keyring + signatureVerificationKR, err := d.getSignatureVerificationKeyring([]string{parentLink.SignatureEmail}, parentNodeKR) + if err != nil { + return "", fmt.Errorf("failed to get signature verification keyring: %w", err) + } + + parentHashKey, err := parentLink.GetHashKey(parentNodeKR, signatureVerificationKR) + if err != nil { + return "", fmt.Errorf("failed to get parent hash key: %w", err) + } + + nameHash, err := proton.GetNameHash(name, parentHashKey) + if err != nil { + return "", fmt.Errorf("failed to generate name hash: %w", err) + } + + return nameHash, nil +} + +func reencryptKeyPacket(srcKR, dstKR, _ *crypto.KeyRing, passphrase string) (string, error) { // addrKR (3) + oldSplitMessage, err := crypto.NewPGPSplitMessageFromArmored(passphrase) + if err != nil { + return "", err + } + + sessionKey, err := srcKR.DecryptSessionKey(oldSplitMessage.KeyPacket) + if err != nil { + return "", err + } + + newKeyPacket, err := dstKR.EncryptSessionKey(sessionKey) + if err != nil { + return "", err + } + + newSplitMessage := crypto.NewPGPSplitMessage(newKeyPacket, oldSplitMessage.DataPacket) + + return newSplitMessage.GetArmored() +} + +func (d *ProtonDrive) checkCircularMove(ctx context.Context, srcLinkID, dstParentLinkID string) error { + currentLinkID := dstParentLinkID + + for currentLinkID != "" && currentLinkID != d.RootLink.LinkID { + if currentLinkID == srcLinkID { + return fmt.Errorf("cannot move folder into itself or its subfolder") + } + + currentLink, err := d.getLink(ctx, currentLinkID) + if err != nil { + return err + } + currentLinkID = currentLink.ParentLinkID + } + + return nil +} diff --git a/go.mod b/go.mod index 7d27d07b1..67590deaf 100644 --- a/go.mod +++ b/go.mod @@ -82,18 +82,32 @@ require ( require ( cloud.google.com/go/compute/metadata v0.7.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect + github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect + github.com/ProtonMail/gluon v0.17.1-0.20230724134000-308be39be96e // indirect + github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect + github.com/ProtonMail/go-srp v0.0.7 // indirect + github.com/ProtonMail/gopenpgp/v2 v2.9.0 // indirect + github.com/PuerkitoBio/goquery v1.10.3 // indirect github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect + github.com/andybalholm/cascadia v1.3.3 // indirect + github.com/bradenaw/juniper v0.15.3 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/cronokirby/saferith v0.33.0 // indirect github.com/ebitengine/purego v0.8.4 // indirect + github.com/emersion/go-message v0.18.2 // indirect + github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff // indirect + github.com/henrybear327/go-proton-api v1.0.0 // indirect github.com/lanrat/extsort v1.0.2 // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/minio/minlz v1.0.0 // indirect github.com/minio/xxml v0.0.3 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/otiai10/mint v1.6.3 // indirect + github.com/relvacode/iso8601 v1.6.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect ) @@ -186,6 +200,7 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-version v1.6.0 // indirect + github.com/henrybear327/Proton-API-Bridge v1.0.0 github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/ipfs/go-cid v0.5.0 github.com/jackc/pgpassfile v1.0.0 // indirect @@ -269,4 +284,8 @@ require ( lukechampine.com/blake3 v1.1.7 // indirect ) +replace github.com/ProtonMail/go-proton-api => github.com/henrybear327/go-proton-api v1.0.0 + +replace github.com/cronokirby/saferith => github.com/Da3zKi7/saferith v0.33.0-fixed + // replace github.com/OpenListTeam/115-sdk-go => ../../OpenListTeam/115-sdk-go diff --git a/go.sum b/go.sum index d631b5b65..c6149c002 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83 github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Da3zKi7/saferith v0.33.0-fixed h1:fnIWTk7EP9mZAICf7aQjeoAwpfrlCrkOvqmi6CbWdTk= +github.com/Da3zKi7/saferith v0.33.0-fixed/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA= github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd h1:nzE1YQBdx1bq9IlZinHa+HVffy+NmVRoKr+wHN8fpLE= github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd/go.mod h1:C8yoIfvESpM3GD07OCHU7fqI7lhwyZ2Td1rbNbTAhnc= github.com/OpenListTeam/115-sdk-go v0.2.2 h1:JCrGHqQjBX3laOA6Hw4CuBovSg7g+FC5s0LEAYsRciU= @@ -53,8 +55,22 @@ github.com/OpenListTeam/times v0.1.0 h1:qknxw+qj5CYKgXAwydA102UEpPcpU8TYNGRmwRyP github.com/OpenListTeam/times v0.1.0/go.mod h1:Jx7qen5NCYzKk2w14YuvU48YYMcPa1P9a+EJePC15Pc= github.com/OpenListTeam/wopan-sdk-go v0.1.5 h1:iKKcVzIqBgtGDbn0QbdWrCazSGxXFmYFyrnFBG+U8dI= github.com/OpenListTeam/wopan-sdk-go v0.1.5/go.mod h1:otynv0CgSNUClPpUgZ44qCZGcMRe0dc83Pkk65xAunI= +github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I= +github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug= +github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo= +github.com/ProtonMail/gluon v0.17.1-0.20230724134000-308be39be96e h1:lCsqUUACrcMC83lg5rTo9Y0PnPItE61JSfvMyIcANwk= +github.com/ProtonMail/gluon v0.17.1-0.20230724134000-308be39be96e/go.mod h1:Og5/Dz1MiGpCJn51XujZwxiLG7WzvvjE5PRpZBQmAHo= +github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= +github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= +github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI= +github.com/ProtonMail/go-srp v0.0.7/go.mod h1:giCp+7qRnMIcCvI6V6U3S1lDDXDQYx2ewJ6F/9wdlJk= +github.com/ProtonMail/gopenpgp/v2 v2.9.0 h1:ruLzBmwe4dR1hdnrsEJ/S7psSBmV15gFttFUPP/+/kE= +github.com/ProtonMail/gopenpgp/v2 v2.9.0/go.mod h1:IldDyh9Hv1ZCCYatTuuEt1XZJ0OPjxLpTarDfglih7s= +github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo= +github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg= github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= @@ -72,6 +88,8 @@ github.com/andreburgaud/crypt2go v1.8.0/go.mod h1:L5nfShQ91W78hOWhUH2tlGRPO+POAP github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 h1:8PmGpDEZl9yDpcdEr6Odf23feCxK3LNUNMxjXg41pZQ= github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= +github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/aws/aws-sdk-go v1.38.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= @@ -164,6 +182,9 @@ github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/bradenaw/juniper v0.15.3 h1:RHIAMEDTpvmzV1wg1jMAHGOoI2oJUSPx3lxRldXnFGo= +github.com/bradenaw/juniper v0.15.3/go.mod h1:UX4FX57kVSaDp4TPqvSjkAAewmRFAfXf27BOs5z9dq8= +github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= @@ -198,6 +219,7 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/city404/v6-public-rpc-proto/go v0.0.0-20240817070657-90f8e24b653e h1:GLC8iDDcbt1H8+RkNao2nRGjyNTIo81e1rAJT9/uWYA= github.com/city404/v6-public-rpc-proto/go v0.0.0-20240817070657-90f8e24b653e/go.mod h1:ln9Whp+wVY/FTbn2SK0ag+SKD2fC0yQCF/Lqowc1LmU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= @@ -235,6 +257,10 @@ github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564 h1:I6KUy4CI6hHjqnyJL github.com/dustinxie/ecc v0.0.0-20210511000915-959544187564/go.mod h1:yekO+3ZShy19S+bsmnERmznGy9Rfg6dWWWpiGJjNAz8= github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/emersion/go-message v0.18.2 h1:rl55SQdjd9oJcIoQNhubD2Acs1E6IzlZISRTK7x/Lpg= +github.com/emersion/go-message v0.18.2/go.mod h1:XpJyL70LwRvq2a8rVbHXikPgKj8+aI0kGdHlg16ibYA= +github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff h1:4N8wnS3f1hNHSmFD5zgFkWCyA4L1kCDkImPAtK7D6tg= +github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= @@ -375,6 +401,10 @@ github.com/hekmon/cunits/v2 v2.1.0 h1:k6wIjc4PlacNOHwKEMBgWV2/c8jyD4eRMs5mR1BBhI github.com/hekmon/cunits/v2 v2.1.0/go.mod h1:9r1TycXYXaTmEWlAIfFV8JT+Xo59U96yUJAYHxzii2M= github.com/hekmon/transmissionrpc/v3 v3.0.0 h1:0Fb11qE0IBh4V4GlOwHNYpqpjcYDp5GouolwrpmcUDQ= github.com/hekmon/transmissionrpc/v3 v3.0.0/go.mod h1:38SlNhFzinVUuY87wGj3acOmRxeYZAZfrj6Re7UgCDg= +github.com/henrybear327/Proton-API-Bridge v1.0.0 h1:gjKAaWfKu++77WsZTHg6FUyPC5W0LTKWQciUm8PMZb0= +github.com/henrybear327/Proton-API-Bridge v1.0.0/go.mod h1:gunH16hf6U74W2b9CGDaWRadiLICsoJ6KRkSt53zLts= +github.com/henrybear327/go-proton-api v1.0.0 h1:zYi/IbjLwFAW7ltCeqXneUGJey0TN//Xo851a/BgLXw= +github.com/henrybear327/go-proton-api v1.0.0/go.mod h1:w63MZuzufKcIZ93pwRgiOtxMXYafI8H74D77AxytOBc= github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI= github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -567,6 +597,8 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rclone/rclone v1.70.3 h1:rg/WNh4DmSVZyKP2tHZ4lAaWEyMi7h/F0r7smOMA3IE= github.com/rclone/rclone v1.70.3/go.mod h1:nLyN+hpxAsQn9Rgt5kM774lcRDad82x/KqQeBZ83cMo= +github.com/relvacode/iso8601 v1.6.0 h1:eFXUhMJN3Gz8Rcq82f9DTMW0svjtAVuIEULglM7QHTU= +github.com/relvacode/iso8601 v1.6.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/rfjakob/eme v1.1.2 h1:SxziR8msSOElPayZNFfQw4Tjx/Sbaeeh3eRvrHVMUs4= github.com/rfjakob/eme v1.1.2/go.mod h1:cVvpasglm/G3ngEfcfT/Wt0GwhkuO32pf/poW6Nyk1k= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -695,6 +727,7 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= @@ -754,10 +787,12 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -800,6 +835,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -818,6 +854,7 @@ golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXct golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= @@ -833,6 +870,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= From eaa95ea4a68e3468c1ccaa33a63e96ea1ab8c384 Mon Sep 17 00:00:00 2001 From: "D@' 3z K!7" <99719341+Da3zKi7@users.noreply.github.com> Date: Sun, 28 Sep 2025 00:24:06 -0600 Subject: [PATCH 02/26] Update drivers/proton_drive/util.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: D@' 3z K!7 <99719341+Da3zKi7@users.noreply.github.com> --- drivers/proton_drive/util.go | 1 - 1 file changed, 1 deletion(-) diff --git a/drivers/proton_drive/util.go b/drivers/proton_drive/util.go index 67c9de633..0b5b173e7 100644 --- a/drivers/proton_drive/util.go +++ b/drivers/proton_drive/util.go @@ -83,7 +83,6 @@ func (d *ProtonDrive) searchByPath(ctx context.Context, fullPath string, isFolde entries, err := d.protonDrive.ListDirectory(ctx, currentLink.LinkID) if err != nil { return nil, fmt.Errorf("failed to list directory: %w", err) - } found := false From a209a85a6a9db3e17cdbd29db543c237b5b548a3 Mon Sep 17 00:00:00 2001 From: KirCute <951206789@qq.com> Date: Mon, 29 Sep 2025 16:25:15 +0800 Subject: [PATCH 03/26] chore --- drivers/proton_drive/driver.go | 58 +++++++++-------------- drivers/proton_drive/types.go | 9 ---- drivers/proton_drive/util.go | 84 ++++++++++------------------------ 3 files changed, 46 insertions(+), 105 deletions(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index 476270ab5..747ef4652 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -24,7 +24,6 @@ import ( "time" "github.com/OpenListTeam/OpenList/v4/internal/driver" - "github.com/OpenListTeam/OpenList/v4/internal/errs" "github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/OpenListTeam/OpenList/v4/internal/stream" "github.com/OpenListTeam/OpenList/v4/pkg/utils" @@ -54,11 +53,11 @@ type ProtonDrive struct { tokenMutex sync.RWMutex c *proton.Client - //m *proton.Manager + // m *proton.Manager credentialCacheFile string - //userKR *crypto.KeyRing + // userKR *crypto.KeyRing addrKRs map[string]*crypto.KeyRing addrData map[string]proton.Address @@ -78,7 +77,6 @@ func (d *ProtonDrive) GetAddition() driver.Additional { } func (d *ProtonDrive) Init(ctx context.Context) error { - defer func() { if r := recover(); r != nil { fmt.Printf("ProtonDrive initialization panic: %v", r) @@ -92,7 +90,7 @@ func (d *ProtonDrive) Init(ctx context.Context) error { return fmt.Errorf("password is required") } - //fmt.Printf("ProtonDrive Init: Username=%s, TwoFACode=%s", d.Username, d.TwoFACode) + // fmt.Printf("ProtonDrive Init: Username=%s, TwoFACode=%s", d.Username, d.TwoFACode) if ctx == nil { return fmt.Errorf("context cannot be nil") @@ -133,7 +131,7 @@ func (d *ProtonDrive) Init(ctx context.Context) error { return fmt.Errorf("failed to create login credentials, FirstLoginCredential cannot be nil") } - //fmt.Printf("Calling NewProtonDrive...") + // fmt.Printf("Calling NewProtonDrive...") protonDrive, credentials, err := proton_api_bridge.NewProtonDrive( ctx, @@ -222,13 +220,13 @@ func (d *ProtonDrive) List(ctx context.Context, dir model.Obj, args model.ListAr return nil, fmt.Errorf("failed to list directory: %w", err) } - //fmt.Printf("Found %d entries for path %s\n", len(entries), dir.GetPath()) - //fmt.Printf("Found %d entries\n", len(entries)) + // fmt.Printf("Found %d entries for path %s\n", len(entries), dir.GetPath()) + // fmt.Printf("Found %d entries\n", len(entries)) if len(entries) == 0 { emptySlice := []model.Obj{} - //fmt.Printf("Returning empty slice (entries): %+v\n", emptySlice) + // fmt.Printf("Returning empty slice (entries): %+v\n", emptySlice) return emptySlice, nil } @@ -299,7 +297,6 @@ func (d *ProtonDrive) Move(ctx context.Context, srcObj, dstDir model.Obj) (model } 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") } @@ -375,13 +372,7 @@ func (d *ProtonDrive) Put(ctx context.Context, dstDir model.Obj, file model.File parentLinkID = link.LinkID } - tempFile, err := utils.CreateTempFile(file, file.GetSize()) - if err != nil { - return nil, fmt.Errorf("failed to create temp file: %w", err) - } - defer tempFile.Close() - - err = d.uploadFile(ctx, parentLinkID, file.GetName(), tempFile, file.GetSize(), up) + err := d.uploadFile(ctx, parentLinkID, file, up) if err != nil { return nil, err } @@ -395,26 +386,19 @@ func (d *ProtonDrive) Put(ctx context.Context, dstDir model.Obj, file model.File return uploadedObj, nil } -func (d *ProtonDrive) GetArchiveMeta(ctx context.Context, obj model.Obj, args model.ArchiveArgs) (model.ArchiveMeta, error) { - // TODO get archive file meta-info, return errs.NotImplement to use an internal archive tool, optional - return nil, errs.NotImplement -} - -func (d *ProtonDrive) ListArchive(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) ([]model.Obj, error) { - // TODO list args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional - return nil, errs.NotImplement -} - -func (d *ProtonDrive) Extract(ctx context.Context, obj model.Obj, args model.ArchiveInnerArgs) (*model.Link, error) { - // TODO return link of file args.InnerPath in the archive obj, return errs.NotImplement to use an internal archive tool, optional - return nil, errs.NotImplement -} - -func (d *ProtonDrive) ArchiveDecompress(ctx context.Context, srcObj, dstDir model.Obj, args model.ArchiveDecompressArgs) ([]model.Obj, error) { - // TODO extract args.InnerPath path in the archive srcObj to the dstDir location, optional - // a folder with the same name as the archive file needs to be created to store the extracted results if args.PutIntoNewDir - // return errs.NotImplement to use an internal archive tool - return nil, errs.NotImplement +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) diff --git a/drivers/proton_drive/types.go b/drivers/proton_drive/types.go index 17e47172a..6313cfd71 100644 --- a/drivers/proton_drive/types.go +++ b/drivers/proton_drive/types.go @@ -16,10 +16,8 @@ D@' 3z K!7 - The King Of Cracking */ import ( - "io" "time" - "github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/henrybear327/go-proton-api" ) @@ -73,13 +71,6 @@ type MoveRequest struct { ContentHash *string `json:"ContentHash"` // Maybe null } -type progressReader struct { - reader io.Reader - total int64 - current int64 - callback driver.UpdateProgress -} - type RenameRequest struct { Name string `json:"Name"` // PGP encrypted name NameSignatureEmail string `json:"NameSignatureEmail"` // User's signature email diff --git a/drivers/proton_drive/util.go b/drivers/proton_drive/util.go index 0b5b173e7..a572ec98c 100644 --- a/drivers/proton_drive/util.go +++ b/drivers/proton_drive/util.go @@ -103,63 +103,38 @@ func (d *ProtonDrive) searchByPath(ctx context.Context, fullPath string, isFolde return currentLink, nil } -func (pr *progressReader) Read(p []byte) (int, error) { - n, err := pr.reader.Read(p) - pr.current += int64(n) - - if pr.callback != nil { - percentage := float64(pr.current) / float64(pr.total) * 100 - pr.callback(percentage) - } - - return n, err -} - -func (d *ProtonDrive) uploadFile(ctx context.Context, parentLinkID, fileName string, file *os.File, size int64, up driver.UpdateProgress) error { - - fileInfo, err := file.Stat() - if err != nil { - return fmt.Errorf("failed to get file info: %w", err) - } - - _, err = d.protonDrive.GetLink(ctx, parentLinkID) +func (d *ProtonDrive) uploadFile(ctx context.Context, parentLinkID string, file model.FileStreamer, up driver.UpdateProgress) error { + _, err := d.protonDrive.GetLink(ctx, parentLinkID) if err != nil { return fmt.Errorf("failed to get parent link: %w", err) } var reader io.Reader - // Use buffered reader with larger buffer for better performance var bufferSize int // File > 100MB (default) - if size > d.ChunkSize*1024*1024 { - + if file.GetSize() > d.ChunkSize*1024*1024 { // 256KB for large files bufferSize = 256 * 1024 - // File > 10MB - } else if size > 10*1024*1024 { - + } else if file.GetSize() > 10*1024*1024 { // 128KB for medium files bufferSize = 128 * 1024 } else { - // 64KB for small files bufferSize = 64 * 1024 } - //reader = bufio.NewReader(file) + // reader = bufio.NewReader(file) reader = bufio.NewReaderSize(file, bufferSize) - - reader = &progressReader{ - reader: reader, - total: size, - current: 0, - callback: up, + reader = &driver.ReaderUpdatingProgress{ + Reader: file, + UpdateProgress: up, } + reader = driver.NewLimitedUploadStream(ctx, reader) - _, _, err = d.protonDrive.UploadFileByReader(ctx, parentLinkID, fileName, fileInfo.ModTime(), reader, 0) + _, _, err = d.protonDrive.UploadFileByReader(ctx, parentLinkID, file.GetName(), file.ModTime(), reader, 0) if err != nil { return fmt.Errorf("failed to upload file: %w", err) } @@ -169,7 +144,6 @@ func (d *ProtonDrive) uploadFile(ctx context.Context, parentLinkID, fileName str func (d *ProtonDrive) ensureTempServer() error { if d.tempServer != nil { - // Already running return nil } @@ -275,7 +249,7 @@ func (d *ProtonDrive) handleTempDownload(w http.ResponseWriter, r *http.Request) w.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(link.Name))) // Setting fileName is needed since ProtonDrive fileName is more like a random string - //w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", link.Name)) + // w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", link.Name)) w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", info.FileName)) w.Header().Set("Accept-Ranges", "bytes") @@ -300,7 +274,6 @@ func (d *ProtonDrive) generateDownloadToken(linkID, fileName string) string { d.tokenMutex.Unlock() go func() { - // Token expires in 1 hour time.Sleep(1 * time.Hour) d.tokenMutex.Lock() @@ -379,7 +352,6 @@ func parseRange(rangeHeader string, size int64) ([]httpRange, error) { } func (d *ProtonDrive) encryptFileName(ctx context.Context, name string, parentLinkID string) (string, error) { - parentLink, err := d.getLink(ctx, parentLinkID) if err != nil { return "", fmt.Errorf("failed to get parent link: %w", err) @@ -406,7 +378,6 @@ func (d *ProtonDrive) encryptFileName(ctx context.Context, name string, parentLi } func (d *ProtonDrive) generateFileNameHash(ctx context.Context, name string, parentLinkID string) (string, error) { - parentLink, err := d.getLink(ctx, parentLinkID) if err != nil { return "", fmt.Errorf("failed to get parent link: %w", err) @@ -500,7 +471,6 @@ var ( ) func getAccountKRs(ctx context.Context, c *proton.Client, keyPass, saltedKeyPass []byte) (*crypto.KeyRing, map[string]*crypto.KeyRing, map[string]proton.Address, []byte, error) { - user, err := c.GetUser(ctx) if err != nil { return nil, nil, nil, nil, err @@ -535,7 +505,6 @@ func getAccountKRs(ctx context.Context, c *proton.Client, keyPass, saltedKeyPass userKR, addrKRs, err := proton.Unlock(user, addrsArr, saltedKeyPass, nil) if err != nil { return nil, nil, nil, nil, err - } else if userKR.CountDecryptionEntities() == 0 { return nil, nil, nil, nil, ErrFailedToUnlockUserKeys } @@ -592,7 +561,7 @@ func (d *ProtonDrive) addKeysFromKR(kr *crypto.KeyRing, newKRs ...*crypto.KeyRin } func (d *ProtonDrive) DirectRename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) { - //fmt.Printf("DEBUG DirectRename: path=%s, newName=%s", srcObj.GetPath(), newName) + // fmt.Printf("DEBUG DirectRename: path=%s, newName=%s", srcObj.GetPath(), newName) if d.MainShare == nil || d.DefaultAddrKR == nil { return nil, fmt.Errorf("missing required fields: MainShare=%v, DefaultAddrKR=%v", @@ -649,7 +618,6 @@ func (d *ProtonDrive) DirectRename(ctx context.Context, srcObj model.Obj, newNam } func (d *ProtonDrive) executeRenameAPI(ctx context.Context, linkID string, req RenameRequest) error { - renameURL := fmt.Sprintf(d.apiBase+"/drive/v2/volumes/%s/links/%s/rename", d.MainShare.VolumeID, linkID) @@ -694,20 +662,20 @@ func (d *ProtonDrive) executeRenameAPI(ctx context.Context, linkID string, req R } func (d *ProtonDrive) executeMoveAPI(ctx context.Context, linkID string, req MoveRequest) error { - //fmt.Printf("DEBUG Move Request - Name: %s\n", req.Name) - //fmt.Printf("DEBUG Move Request - Hash: %s\n", req.Hash) - //fmt.Printf("DEBUG Move Request - OriginalHash: %s\n", req.OriginalHash) - //fmt.Printf("DEBUG Move Request - ParentLinkID: %s\n", req.ParentLinkID) + // fmt.Printf("DEBUG Move Request - Name: %s\n", req.Name) + // fmt.Printf("DEBUG Move Request - Hash: %s\n", req.Hash) + // fmt.Printf("DEBUG Move Request - OriginalHash: %s\n", req.OriginalHash) + // fmt.Printf("DEBUG Move Request - ParentLinkID: %s\n", req.ParentLinkID) - //fmt.Printf("DEBUG Move Request - Name length: %d\n", len(req.Name)) - //fmt.Printf("DEBUG Move Request - NameSignatureEmail: %s\n", req.NameSignatureEmail) - //fmt.Printf("DEBUG Move Request - ContentHash: %v\n", req.ContentHash) - //fmt.Printf("DEBUG Move Request - NodePassphrase length: %d\n", len(req.NodePassphrase)) - //fmt.Printf("DEBUG Move Request - NodePassphraseSignature length: %d\n", len(req.NodePassphraseSignature)) + // fmt.Printf("DEBUG Move Request - Name length: %d\n", len(req.Name)) + // fmt.Printf("DEBUG Move Request - NameSignatureEmail: %s\n", req.NameSignatureEmail) + // fmt.Printf("DEBUG Move Request - ContentHash: %v\n", req.ContentHash) + // fmt.Printf("DEBUG Move Request - NodePassphrase length: %d\n", len(req.NodePassphrase)) + // fmt.Printf("DEBUG Move Request - NodePassphraseSignature length: %d\n", len(req.NodePassphraseSignature)) - //fmt.Printf("DEBUG Move Request - SrcLinkID: %s\n", linkID) - //fmt.Printf("DEBUG Move Request - DstParentLinkID: %s\n", req.ParentLinkID) - //fmt.Printf("DEBUG Move Request - ShareID: %s\n", d.MainShare.ShareID) + // fmt.Printf("DEBUG Move Request - SrcLinkID: %s\n", linkID) + // fmt.Printf("DEBUG Move Request - DstParentLinkID: %s\n", req.ParentLinkID) + // fmt.Printf("DEBUG Move Request - ShareID: %s\n", d.MainShare.ShareID) srcLink, _ := d.getLink(ctx, linkID) if srcLink != nil && srcLink.ParentLinkID == req.ParentLinkID { @@ -754,7 +722,7 @@ func (d *ProtonDrive) executeMoveAPI(ctx context.Context, linkID string, req Mov } func (d *ProtonDrive) DirectMove(ctx context.Context, srcObj model.Obj, dstDir model.Obj) (model.Obj, error) { - //fmt.Printf("DEBUG DirectMove: srcPath=%s, dstPath=%s", srcObj.GetPath(), dstDir.GetPath()) + // fmt.Printf("DEBUG DirectMove: srcPath=%s, dstPath=%s", srcObj.GetPath(), dstDir.GetPath()) srcLink, err := d.searchByPath(ctx, srcObj.GetPath(), srcObj.IsDir()) if err != nil { @@ -773,7 +741,6 @@ func (d *ProtonDrive) DirectMove(ctx context.Context, srcObj model.Obj, dstDir m } if srcObj.IsDir() { - // Check if destination is a descendant of source if err := d.checkCircularMove(ctx, srcLink.LinkID, dstParentLinkID); err != nil { return nil, err @@ -871,7 +838,6 @@ func (d *ProtonDrive) reencryptNodePassphrase(ctx context.Context, srcLink *prot } func (d *ProtonDrive) generateNameHash(ctx context.Context, name string, parentLinkID string) (string, error) { - parentLink, err := d.getLink(ctx, parentLinkID) if err != nil { return "", fmt.Errorf("failed to get parent link: %w", err) From 8caefc359bbf2f8b604dcd7ee2915a599adb99a6 Mon Sep 17 00:00:00 2001 From: Da3zKi7 Date: Tue, 30 Sep 2025 10:18:36 -0600 Subject: [PATCH 04/26] 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 --- drivers/proton_drive/driver.go | 49 ++++++++++++++++++++++++++++++++-- drivers/proton_drive/meta.go | 13 +++++---- drivers/proton_drive/util.go | 49 +++++++++++++++++++++++++++++++--- 3 files changed, 100 insertions(+), 11 deletions(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index 747ef4652..1dd65fc2f 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -48,7 +48,6 @@ type ProtonDrive struct { webDriveAV string tempServer *http.Server - tempServerPort int downloadTokens map[string]*downloadInfo tokenMutex sync.RWMutex @@ -261,11 +260,57 @@ func (d *ProtonDrive) Link(ctx context.Context, file model.Obj, args model.LinkA URL: fmt.Sprintf("protondrive://download/%s", link.LinkID), }, nil */ + //fmt.Printf("d.TempServerPublicHost: %v\n", d.TempServerPublicHost) + //fmt.Printf("d.TempServerPublicPort: %d\n", d.TempServerPublicPort) + + // Use public host and port for the URL returned to clients return &model.Link{ - URL: fmt.Sprintf("http://localhost:%d/temp/%s", d.tempServerPort, token), + URL: fmt.Sprintf("http://%s:%d/temp/%s", + d.TempServerPublicHost, d.TempServerPublicPort, token), }, nil } +//Causes 500 error, but leave it because is an alternative +/* func (d *ProtonDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + link, err := d.searchByPath(ctx, file.GetPath(), false) + if err != nil { + return nil, err + } + token := d.generateDownloadToken(link.LinkID, file.GetName()) + size := file.GetSize() + + 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 + } + d.tokenMutex.RLock() + info, exists := d.downloadTokens[token] + d.tokenMutex.RUnlock() + if !exists { + return nil, errors.New("invalid or expired token") + } + link, err := d.protonDrive.GetLink(rangeCtx, info.LinkID) + if err != nil { + return nil, fmt.Errorf("failed get file link: %+v", err) + } + 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 + } + + return &model.Link{ + RangeReader: &model.FileRangeReader{ + RangeReaderIF: stream.RateLimitRangeReaderFunc(rangeReaderFunc), + }, + }, nil +} */ + func (d *ProtonDrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) { var parentLinkID string diff --git a/drivers/proton_drive/meta.go b/drivers/proton_drive/meta.go index 89e485df3..8ac34f188 100644 --- a/drivers/proton_drive/meta.go +++ b/drivers/proton_drive/meta.go @@ -24,10 +24,13 @@ type Addition struct { driver.RootPath //driver.RootID - Username string `json:"username" required:"true" type:"string"` - Password string `json:"password" required:"true" type:"string"` - TwoFACode string `json:"two_fa_code,omitempty" type:"string"` - ChunkSize int64 `json:"chunk_size" type:"number" default:"100"` + Username string `json:"username" required:"true" type:"string"` + Password string `json:"password" required:"true" type:"string"` + TwoFACode string `json:"two_fa_code,omitempty" type:"string"` + ChunkSize int64 `json:"chunk_size" type:"number" default:"100"` + TempServerListenPort int `json:"temp_server_listen_port" type:"number" default:"0" help:"Internal port for temp server to bind to (0 for auto, preferred = 8080)"` + TempServerPublicPort int `json:"temp_server_public_port" type:"number" default:"0" help:"External port that clients will connect to (0 for auto, preferred = 8080)"` + TempServerPublicHost string `json:"temp_server_public_host" type:"string" default:"127.0.0.1" help:"External domain/IP that clients will connect to i.e. 192.168.1.5 (default = 127.0.0.1)"` } type Config struct { @@ -45,7 +48,7 @@ var config = driver.Config{ Name: "ProtonDrive", LocalSort: false, //OnlyLocal: false, - OnlyProxy: false, + OnlyProxy: false, //Please leave it disabled, true breaks stream / download NoCache: false, NoUpload: false, NeedMs: false, diff --git a/drivers/proton_drive/util.go b/drivers/proton_drive/util.go index a572ec98c..5f370de59 100644 --- a/drivers/proton_drive/util.go +++ b/drivers/proton_drive/util.go @@ -148,11 +148,50 @@ func (d *ProtonDrive) ensureTempServer() error { return nil } - listener, err := net.Listen("tcp", ":0") + // Use configured listen port, or auto-assign if 0 + if d.TempServerListenPort == 0 { + // Try preferred port 8080 first + listener, err := net.Listen("tcp", ":8080") + if err == nil { + // Port 8080 is available (default) + d.TempServerListenPort = 8080 + listener.Close() + } else { + // Port 8080 not available, auto-assign any available port + listener, err := net.Listen("tcp", ":0") + if err != nil { + return fmt.Errorf("failed to get available port: %w", err) + } + d.TempServerListenPort = listener.Addr().(*net.TCPAddr).Port + listener.Close() + } + } + + //If public port is default, use the same port as listen + if d.TempServerPublicPort == 0 { + d.TempServerPublicPort = d.TempServerListenPort + } else { + // Validate that the configured public port is available + testListener, err := net.Listen("tcp", fmt.Sprintf(":%d", d.TempServerPublicPort)) + if err != nil { + // Public port not available, fall back to listen port + fmt.Printf("ProtonDrive: configured public port %d not available, falling back to listen port %d\n", + d.TempServerPublicPort, d.TempServerListenPort) + d.TempServerPublicPort = d.TempServerListenPort + + fmt.Printf("ProtonDrive: if using container like Docker, NAT env or VM \nplease configure external port forwarding/mapping %d -> %d \n", d.TempServerListenPort, d.TempServerPublicPort) + } else { + // Port is available, close the listener + testListener.Close() + } + } + + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", d.TempServerListenPort)) if err != nil { - return err + return fmt.Errorf("failed to listen on port %d: %w", d.TempServerListenPort, err) } - d.tempServerPort = listener.Addr().(*net.TCPAddr).Port + + //fmt.Printf("d.TempServerListenPort: %d\n", d.TempServerListenPort) mux := http.NewServeMux() mux.HandleFunc("/temp/", d.handleTempDownload) @@ -162,7 +201,9 @@ func (d *ProtonDrive) ensureTempServer() error { } go func() { - d.tempServer.Serve(listener) + if err := d.tempServer.Serve(listener); err != nil && err != http.ErrServerClosed { + fmt.Printf("ProtonDrive temp server error: %v\n", err) + } }() return nil From 386961d2c4ed6f8b4b23902abc6d5b82106d0556 Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Fri, 10 Oct 2025 11:00:22 +0800 Subject: [PATCH 05/26] feat(proton_drive): refactor directory handling and improve link retrieval --- drivers/proton_drive/driver.go | 104 +++++---------------------------- drivers/proton_drive/meta.go | 30 ++-------- drivers/proton_drive/util.go | 4 +- 3 files changed, 22 insertions(+), 116 deletions(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index 1dd65fc2f..3c239294b 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -19,6 +19,7 @@ import ( "context" "encoding/base64" "fmt" + "io" "net/http" "sync" "time" @@ -26,6 +27,7 @@ import ( "github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/model" "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" @@ -61,7 +63,6 @@ type ProtonDrive struct { addrData map[string]proton.Address MainShare *proton.Share - RootLink *proton.Link DefaultAddrKR *crypto.KeyRing MainShareKR *crypto.KeyRing @@ -184,7 +185,9 @@ func (d *ProtonDrive) Init(ctx context.Context) error { } d.MainShare = protonDrive.MainShare - d.RootLink = protonDrive.RootLink + if d.RootFolderID == "root" { + d.RootFolderID = protonDrive.RootLink.LinkID + } d.MainShareKR = protonDrive.MainShareKR d.DefaultAddrKR = protonDrive.DefaultAddrKR d.addrKRs = addrKRs @@ -201,38 +204,15 @@ func (d *ProtonDrive) Drop(ctx context.Context) error { } func (d *ProtonDrive) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { - var linkID string - - if dir.GetPath() == "/" { - linkID = d.protonDrive.RootLink.LinkID - } else { - - link, err := d.searchByPath(ctx, dir.GetPath(), true) - if err != nil { - return nil, err - } - linkID = link.LinkID - } - - entries, err := d.protonDrive.ListDirectory(ctx, linkID) + entries, err := d.protonDrive.ListDirectory(ctx, dir.GetID()) if err != nil { return nil, fmt.Errorf("failed to list directory: %w", err) } - // fmt.Printf("Found %d entries for path %s\n", len(entries), dir.GetPath()) - // fmt.Printf("Found %d entries\n", len(entries)) - - if len(entries) == 0 { - emptySlice := []model.Obj{} - - // fmt.Printf("Returning empty slice (entries): %+v\n", emptySlice) - - return emptySlice, nil - } - - var objects []model.Obj + 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), @@ -245,38 +225,10 @@ func (d *ProtonDrive) List(ctx context.Context, dir model.Obj, args model.ListAr } func (d *ProtonDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { - link, err := d.searchByPath(ctx, file.GetPath(), false) + link, err := d.protonDrive.GetLink(ctx, file.GetID()) if err != nil { - return nil, err + return nil, fmt.Errorf("failed get file link: %+v", err) } - - if err := d.ensureTempServer(); err != nil { - return nil, fmt.Errorf("failed to start temp server: %w", err) - } - - token := d.generateDownloadToken(link.LinkID, file.GetName()) - - /* return &model.Link{ - URL: fmt.Sprintf("protondrive://download/%s", link.LinkID), - }, nil */ - - //fmt.Printf("d.TempServerPublicHost: %v\n", d.TempServerPublicHost) - //fmt.Printf("d.TempServerPublicPort: %d\n", d.TempServerPublicPort) - - // Use public host and port for the URL returned to clients - return &model.Link{ - URL: fmt.Sprintf("http://%s:%d/temp/%s", - d.TempServerPublicHost, d.TempServerPublicPort, token), - }, nil -} - -//Causes 500 error, but leave it because is an alternative -/* func (d *ProtonDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { - link, err := d.searchByPath(ctx, file.GetPath(), false) - if err != nil { - return nil, err - } - token := d.generateDownloadToken(link.LinkID, file.GetName()) size := file.GetSize() rangeReaderFunc := func(rangeCtx context.Context, httpRange http_range.Range) (io.ReadCloser, error) { @@ -284,16 +236,6 @@ func (d *ProtonDrive) Link(ctx context.Context, file model.Obj, args model.LinkA if length < 0 || httpRange.Start+length > size { length = size - httpRange.Start } - d.tokenMutex.RLock() - info, exists := d.downloadTokens[token] - d.tokenMutex.RUnlock() - if !exists { - return nil, errors.New("invalid or expired token") - } - link, err := d.protonDrive.GetLink(rangeCtx, info.LinkID) - if err != nil { - return nil, fmt.Errorf("failed get file link: %+v", err) - } reader, _, _, err := d.protonDrive.DownloadFile(rangeCtx, link, httpRange.Start) if err != nil { return nil, fmt.Errorf("failed start download: %+v", err) @@ -309,27 +251,16 @@ func (d *ProtonDrive) Link(ctx context.Context, file model.Obj, args model.LinkA RangeReaderIF: stream.RateLimitRangeReaderFunc(rangeReaderFunc), }, }, nil -} */ +} func (d *ProtonDrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) { - var parentLinkID string - - if parentDir.GetPath() == "/" { - parentLinkID = d.protonDrive.RootLink.LinkID - } else { - link, err := d.searchByPath(ctx, parentDir.GetPath(), true) - if err != nil { - return nil, err - } - parentLinkID = link.LinkID - } - - _, err := d.protonDrive.CreateNewFolderByID(ctx, parentLinkID, dirName) + 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(), @@ -392,15 +323,10 @@ func (d *ProtonDrive) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model } func (d *ProtonDrive) Remove(ctx context.Context, obj model.Obj) error { - link, err := d.searchByPath(ctx, obj.GetPath(), obj.IsDir()) - if err != nil { - return err - } - if obj.IsDir() { - return d.protonDrive.MoveFolderToTrashByID(ctx, link.LinkID, false) + return d.protonDrive.MoveFolderToTrashByID(ctx, obj.GetID(), false) } else { - return d.protonDrive.MoveFileToTrashByID(ctx, link.LinkID) + return d.protonDrive.MoveFileToTrashByID(ctx, obj.GetID()) } } diff --git a/drivers/proton_drive/meta.go b/drivers/proton_drive/meta.go index 8ac34f188..9e0ed4bce 100644 --- a/drivers/proton_drive/meta.go +++ b/drivers/proton_drive/meta.go @@ -21,9 +21,7 @@ import ( ) type Addition struct { - driver.RootPath - //driver.RootID - + driver.RootID Username string `json:"username" required:"true" type:"string"` Password string `json:"password" required:"true" type:"string"` TwoFACode string `json:"two_fa_code,omitempty" type:"string"` @@ -33,29 +31,11 @@ type Addition struct { TempServerPublicHost string `json:"temp_server_public_host" type:"string" default:"127.0.0.1" help:"External domain/IP that clients will connect to i.e. 192.168.1.5 (default = 127.0.0.1)"` } -type Config struct { - Name string `json:"name"` - LocalSort bool `json:"local_sort"` - OnlyLocal bool `json:"only_local"` - OnlyProxy bool `json:"only_proxy"` - NoCache bool `json:"no_cache"` - NoUpload bool `json:"no_upload"` - NeedMs bool `json:"need_ms"` - DefaultRoot string `json:"default_root"` -} - var config = driver.Config{ - Name: "ProtonDrive", - LocalSort: false, - //OnlyLocal: false, - OnlyProxy: false, //Please leave it disabled, true breaks stream / download - NoCache: false, - NoUpload: false, - NeedMs: false, - DefaultRoot: "/", - CheckStatus: false, - Alert: "", - NoOverwriteUpload: false, + Name: "ProtonDrive", + LocalSort: true, + OnlyProxy: true, + DefaultRoot: "root", } func init() { diff --git a/drivers/proton_drive/util.go b/drivers/proton_drive/util.go index 5f370de59..a2064366c 100644 --- a/drivers/proton_drive/util.go +++ b/drivers/proton_drive/util.go @@ -772,7 +772,7 @@ func (d *ProtonDrive) DirectMove(ctx context.Context, srcObj model.Obj, dstDir m var dstParentLinkID string if dstDir.GetPath() == "/" { - dstParentLinkID = d.RootLink.LinkID + dstParentLinkID = d.RootFolderID } else { dstLink, err := d.searchByPath(ctx, dstDir.GetPath(), true) if err != nil { @@ -933,7 +933,7 @@ func reencryptKeyPacket(srcKR, dstKR, _ *crypto.KeyRing, passphrase string) (str func (d *ProtonDrive) checkCircularMove(ctx context.Context, srcLinkID, dstParentLinkID string) error { currentLinkID := dstParentLinkID - for currentLinkID != "" && currentLinkID != d.RootLink.LinkID { + for currentLinkID != "" && currentLinkID != d.RootFolderID { if currentLinkID == srcLinkID { return fmt.Errorf("cannot move folder into itself or its subfolder") } From 564d25254f00412753a48ea4922f872e602d5d3a Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Fri, 10 Oct 2025 11:28:34 +0800 Subject: [PATCH 06/26] fix(proton_drive): add NoLinkURL configuration option --- drivers/proton_drive/meta.go | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/proton_drive/meta.go b/drivers/proton_drive/meta.go index 9e0ed4bce..7900224c2 100644 --- a/drivers/proton_drive/meta.go +++ b/drivers/proton_drive/meta.go @@ -36,6 +36,7 @@ var config = driver.Config{ LocalSort: true, OnlyProxy: true, DefaultRoot: "root", + NoLinkURL: true, } func init() { From 3d997877801efbf53d0749b9214258c4019910c9 Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Sat, 11 Oct 2025 00:07:48 +0800 Subject: [PATCH 07/26] fix(proton_drive): update file size retrieval and enforce TwoFACode requirement --- drivers/proton_drive/driver.go | 8 +++++++- drivers/proton_drive/meta.go | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index 3c239294b..5409ffd8c 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -229,7 +229,12 @@ func (d *ProtonDrive) Link(ctx context.Context, file model.Obj, args model.LinkA if err != nil { return nil, fmt.Errorf("failed get file link: %+v", err) } - size := file.GetSize() + 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 @@ -250,6 +255,7 @@ func (d *ProtonDrive) Link(ctx context.Context, file model.Obj, args model.LinkA RangeReader: &model.FileRangeReader{ RangeReaderIF: stream.RateLimitRangeReaderFunc(rangeReaderFunc), }, + ContentLength: size, }, nil } diff --git a/drivers/proton_drive/meta.go b/drivers/proton_drive/meta.go index 7900224c2..3ef443852 100644 --- a/drivers/proton_drive/meta.go +++ b/drivers/proton_drive/meta.go @@ -24,7 +24,7 @@ type Addition struct { driver.RootID Username string `json:"username" required:"true" type:"string"` Password string `json:"password" required:"true" type:"string"` - TwoFACode string `json:"two_fa_code,omitempty" type:"string"` + TwoFACode string `json:"two_fa_code" type:"string"` ChunkSize int64 `json:"chunk_size" type:"number" default:"100"` TempServerListenPort int `json:"temp_server_listen_port" type:"number" default:"0" help:"Internal port for temp server to bind to (0 for auto, preferred = 8080)"` TempServerPublicPort int `json:"temp_server_public_port" type:"number" default:"0" help:"External port that clients will connect to (0 for auto, preferred = 8080)"` From 363624b0506493fd6429162fd36d18013d559dfd Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Sat, 11 Oct 2025 00:15:07 +0800 Subject: [PATCH 08/26] feat(proton_drive): add expiration to link response --- drivers/proton_drive/driver.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index 5409ffd8c..305afef1b 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -251,11 +251,13 @@ func (d *ProtonDrive) Link(ctx context.Context, file model.Obj, args model.LinkA }, nil } + expiration := time.Minute return &model.Link{ RangeReader: &model.FileRangeReader{ RangeReaderIF: stream.RateLimitRangeReaderFunc(rangeReaderFunc), }, ContentLength: size, + Expiration: &expiration, }, nil } From 9753fde5fb912829cae9ffbc4dc2354583576928 Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Sat, 11 Oct 2025 00:23:29 +0800 Subject: [PATCH 09/26] fix(proton_drive): handle empty RootFolderID in Init method --- drivers/proton_drive/driver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index 305afef1b..6b7e6961f 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -185,7 +185,7 @@ func (d *ProtonDrive) Init(ctx context.Context) error { } d.MainShare = protonDrive.MainShare - if d.RootFolderID == "root" { + if d.RootFolderID == "root" || d.RootFolderID == "" { d.RootFolderID = protonDrive.RootLink.LinkID } d.MainShareKR = protonDrive.MainShareKR From e867092096ac1242098e52e2a10322e205957438 Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Sat, 11 Oct 2025 01:07:59 +0800 Subject: [PATCH 10/26] fix(proton_drive): update credential handling to use email and reusable login --- drivers/proton_drive/driver.go | 71 ++++++---------------------------- drivers/proton_drive/meta.go | 22 +++++++---- drivers/proton_drive/util.go | 37 ++---------------- 3 files changed, 29 insertions(+), 101 deletions(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index 6b7e6961f..478ef1756 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -40,7 +40,6 @@ type ProtonDrive struct { Addition protonDrive *proton_api_bridge.ProtonDrive - credentials *common.ProtonDriveCredential apiBase string appVersion string @@ -56,8 +55,6 @@ type ProtonDrive struct { c *proton.Client // m *proton.Manager - credentialCacheFile string - // userKR *crypto.KeyRing addrKRs map[string]*crypto.KeyRing addrData map[string]proton.Address @@ -77,104 +74,57 @@ func (d *ProtonDrive) GetAddition() driver.Additional { } func (d *ProtonDrive) Init(ctx context.Context) error { - defer func() { - if r := recover(); r != nil { - fmt.Printf("ProtonDrive initialization panic: %v", r) - } - }() - if d.Username == "" { - return fmt.Errorf("username is required") + if d.Email == "" { + return fmt.Errorf("email is required") } if d.Password == "" { return fmt.Errorf("password is required") } - // fmt.Printf("ProtonDrive Init: Username=%s, TwoFACode=%s", d.Username, d.TwoFACode) - - if ctx == nil { - return fmt.Errorf("context cannot be nil") - } - - cachedCredentials, err := d.loadCachedCredentials() useReusableLogin := false - var reusableCredential *common.ReusableCredentialData + reusableCredential := &d.ReusableCredential - if err == nil && cachedCredentials != nil && - cachedCredentials.UID != "" && cachedCredentials.AccessToken != "" && - cachedCredentials.RefreshToken != "" && cachedCredentials.SaltedKeyPass != "" { + if d.UseReusableLogin && reusableCredential.UID != "" && reusableCredential.AccessToken != "" && + reusableCredential.RefreshToken != "" && reusableCredential.SaltedKeyPass != "" { useReusableLogin = true - reusableCredential = cachedCredentials - } else { - useReusableLogin = false - reusableCredential = &common.ReusableCredentialData{} } config := &common.Config{ AppVersion: d.appVersion, UserAgent: d.userAgent, FirstLoginCredential: &common.FirstLoginCredentialData{ - Username: d.Username, + Username: d.Email, Password: d.Password, TwoFA: d.TwoFACode, }, EnableCaching: true, ConcurrentBlockUploadCount: 5, ConcurrentFileCryptoCount: 2, - UseReusableLogin: false, + UseReusableLogin: useReusableLogin, ReplaceExistingDraft: true, ReusableCredential: reusableCredential, - CredentialCacheFile: d.credentialCacheFile, } - if config.FirstLoginCredential == nil { - return fmt.Errorf("failed to create login credentials, FirstLoginCredential cannot be nil") - } - - // fmt.Printf("Calling NewProtonDrive...") - - protonDrive, credentials, err := proton_api_bridge.NewProtonDrive( + protonDrive, _, err := proton_api_bridge.NewProtonDrive( ctx, config, func(auth proton.Auth) {}, func() {}, ) - if credentials == nil && !useReusableLogin { - return fmt.Errorf("failed to get credentials from NewProtonDrive") - } - if err != nil { return fmt.Errorf("failed to initialize ProtonDrive: %w", err) } - d.protonDrive = protonDrive - - var finalCredentials *common.ProtonDriveCredential - - if useReusableLogin { - - // For reusable login, create credentials from cached data - finalCredentials = &common.ProtonDriveCredential{ - UID: reusableCredential.UID, - AccessToken: reusableCredential.AccessToken, - RefreshToken: reusableCredential.RefreshToken, - SaltedKeyPass: reusableCredential.SaltedKeyPass, - } - - d.credentials = finalCredentials - } else { - d.credentials = credentials - } - clientOptions := []proton.Option{ proton.WithAppVersion(d.appVersion), proton.WithUserAgent(d.userAgent), } manager := proton.New(clientOptions...) - d.c = manager.NewClient(d.credentials.UID, d.credentials.AccessToken, d.credentials.RefreshToken) + d.c = manager.NewClient(d.ReusableCredential.UID, d.ReusableCredential.AccessToken, d.ReusableCredential.RefreshToken) - saltedKeyPassBytes, err := base64.StdEncoding.DecodeString(d.credentials.SaltedKeyPass) + saltedKeyPassBytes, err := base64.StdEncoding.DecodeString(d.ReusableCredential.SaltedKeyPass) if err != nil { return fmt.Errorf("failed to decode salted key pass: %w", err) } @@ -184,6 +134,7 @@ func (d *ProtonDrive) Init(ctx context.Context) error { return fmt.Errorf("failed to get account keyrings: %w", err) } + d.protonDrive = protonDrive d.MainShare = protonDrive.MainShare if d.RootFolderID == "root" || d.RootFolderID == "" { d.RootFolderID = protonDrive.RootLink.LinkID diff --git a/drivers/proton_drive/meta.go b/drivers/proton_drive/meta.go index 3ef443852..2521db166 100644 --- a/drivers/proton_drive/meta.go +++ b/drivers/proton_drive/meta.go @@ -18,17 +18,21 @@ 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 - Username string `json:"username" required:"true" type:"string"` + 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"` TempServerListenPort int `json:"temp_server_listen_port" type:"number" default:"0" help:"Internal port for temp server to bind to (0 for auto, preferred = 8080)"` TempServerPublicPort int `json:"temp_server_public_port" type:"number" default:"0" help:"External port that clients will connect to (0 for auto, preferred = 8080)"` TempServerPublicHost string `json:"temp_server_public_host" type:"string" default:"127.0.0.1" help:"External domain/IP that clients will connect to i.e. 192.168.1.5 (default = 127.0.0.1)"` + + ReusableCredential common.ReusableCredentialData } var config = driver.Config{ @@ -42,13 +46,15 @@ var config = driver.Config{ func init() { op.RegisterDriver(func() driver.Driver { return &ProtonDrive{ - apiBase: "https://drive.proton.me/api", - appVersion: "windows-drive@1.11.3+rclone+proton", - credentialCacheFile: "./data/.prtcrd", - 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", + 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", } }) } diff --git a/drivers/proton_drive/util.go b/drivers/proton_drive/util.go index a2064366c..be9392e37 100644 --- a/drivers/proton_drive/util.go +++ b/drivers/proton_drive/util.go @@ -26,7 +26,6 @@ import ( "mime" "net" "net/http" - "os" "path/filepath" "strconv" "strings" @@ -35,37 +34,9 @@ import ( "github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/model" "github.com/ProtonMail/gopenpgp/v2/crypto" - "github.com/henrybear327/Proton-API-Bridge/common" "github.com/henrybear327/go-proton-api" ) -func (d *ProtonDrive) loadCachedCredentials() (*common.ReusableCredentialData, error) { - if d.credentialCacheFile == "" { - return nil, nil - } - - if _, err := os.Stat(d.credentialCacheFile); os.IsNotExist(err) { - return nil, nil - } - - data, err := os.ReadFile(d.credentialCacheFile) - if err != nil { - return nil, fmt.Errorf("failed to read credential cache file: %w", err) - } - - var credentials common.ReusableCredentialData - if err := json.Unmarshal(data, &credentials); err != nil { - return nil, fmt.Errorf("failed to parse cached credentials: %w", err) - } - - if credentials.UID == "" || credentials.AccessToken == "" || - credentials.RefreshToken == "" || credentials.SaltedKeyPass == "" { - return nil, fmt.Errorf("cached credentials are incomplete") - } - - return &credentials, nil -} - func (d *ProtonDrive) searchByPath(ctx context.Context, fullPath string, isFolder bool) (*proton.Link, error) { if fullPath == "/" { return d.protonDrive.RootLink, nil @@ -676,8 +647,8 @@ func (d *ProtonDrive) executeRenameAPI(ctx context.Context, linkID string, req R httpReq.Header.Set("Accept", d.protonJson) httpReq.Header.Set("X-Pm-Appversion", d.webDriveAV) httpReq.Header.Set("X-Pm-Drive-Sdk-Version", d.sdkVersion) - httpReq.Header.Set("X-Pm-Uid", d.credentials.UID) - httpReq.Header.Set("Authorization", "Bearer "+d.credentials.AccessToken) + httpReq.Header.Set("X-Pm-Uid", d.ReusableCredential.UID) + httpReq.Header.Set("Authorization", "Bearer "+d.ReusableCredential.AccessToken) client := &http.Client{} resp, err := client.Do(httpReq) @@ -736,11 +707,11 @@ func (d *ProtonDrive) executeMoveAPI(ctx context.Context, linkID string, req Mov return fmt.Errorf("failed to create HTTP request: %w", err) } - httpReq.Header.Set("Authorization", "Bearer "+d.credentials.AccessToken) + httpReq.Header.Set("Authorization", "Bearer "+d.ReusableCredential.AccessToken) httpReq.Header.Set("Accept", d.protonJson) httpReq.Header.Set("X-Pm-Appversion", d.webDriveAV) httpReq.Header.Set("X-Pm-Drive-Sdk-Version", d.sdkVersion) - httpReq.Header.Set("X-Pm-Uid", d.credentials.UID) + httpReq.Header.Set("X-Pm-Uid", d.ReusableCredential.UID) httpReq.Header.Set("Content-Type", "application/json") client := &http.Client{} From 9d5a78a5bb7c9d5e5cff0045a347cda001cd5f3c Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Sat, 11 Oct 2025 01:11:40 +0800 Subject: [PATCH 11/26] fix(proton_drive): update credential handling to use reusableCredential variable --- drivers/proton_drive/driver.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index 478ef1756..57445521d 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -122,9 +122,9 @@ func (d *ProtonDrive) Init(ctx context.Context) error { proton.WithUserAgent(d.userAgent), } manager := proton.New(clientOptions...) - d.c = manager.NewClient(d.ReusableCredential.UID, d.ReusableCredential.AccessToken, d.ReusableCredential.RefreshToken) + d.c = manager.NewClient(reusableCredential.UID, reusableCredential.AccessToken, reusableCredential.RefreshToken) - saltedKeyPassBytes, err := base64.StdEncoding.DecodeString(d.ReusableCredential.SaltedKeyPass) + saltedKeyPassBytes, err := base64.StdEncoding.DecodeString(reusableCredential.SaltedKeyPass) if err != nil { return fmt.Errorf("failed to decode salted key pass: %w", err) } From 6254480a0349bc7e7adca965f19fdeb0320f2d26 Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Sat, 11 Oct 2025 19:13:14 +0800 Subject: [PATCH 12/26] fix(proton_drive): update DirectRename to use GetLink for source object retrieval --- drivers/proton_drive/driver.go | 5 +++++ drivers/proton_drive/util.go | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index 57445521d..7b47b2685 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -74,6 +74,11 @@ func (d *ProtonDrive) GetAddition() driver.Additional { } func (d *ProtonDrive) Init(ctx context.Context) error { + defer func() { + if r := recover(); r != nil { + fmt.Printf("ProtonDrive initialization panic: %v", r) + } + }() if d.Email == "" { return fmt.Errorf("email is required") diff --git a/drivers/proton_drive/util.go b/drivers/proton_drive/util.go index be9392e37..43874c172 100644 --- a/drivers/proton_drive/util.go +++ b/drivers/proton_drive/util.go @@ -584,7 +584,7 @@ func (d *ProtonDrive) DirectRename(ctx context.Context, srcObj model.Obj, newNam return nil, fmt.Errorf("protonDrive bridge is nil") } - srcLink, err := d.searchByPath(ctx, srcObj.GetPath(), srcObj.IsDir()) + srcLink, err := d.protonDrive.GetLink(ctx, srcObj.GetID()) if err != nil { return nil, fmt.Errorf("failed to find source: %w", err) } @@ -622,6 +622,7 @@ func (d *ProtonDrive) DirectRename(ctx context.Context, srcObj model.Obj, newNam } return &model.Object{ + ID: srcLink.LinkID, Name: newName, Size: srcObj.GetSize(), Modified: srcObj.ModTime(), From d7ce484817158ecdfac5d7f19fb4caa06253c267 Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Sat, 11 Oct 2025 19:39:20 +0800 Subject: [PATCH 13/26] fix(proton_drive): refactor uploadFile to return model.Obj and handle errors correctly --- drivers/proton_drive/driver.go | 25 +------------------------ drivers/proton_drive/util.go | 16 +++++++++++----- 2 files changed, 12 insertions(+), 29 deletions(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index 7b47b2685..0f6cdfd75 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -295,30 +295,7 @@ func (d *ProtonDrive) Remove(ctx context.Context, obj model.Obj) error { } func (d *ProtonDrive) Put(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { - var parentLinkID string - - if dstDir.GetPath() == "/" { - parentLinkID = d.protonDrive.RootLink.LinkID - } else { - link, err := d.searchByPath(ctx, dstDir.GetPath(), true) - if err != nil { - return nil, err - } - parentLinkID = link.LinkID - } - - err := d.uploadFile(ctx, parentLinkID, file, up) - if err != nil { - return nil, err - } - - uploadedObj := &model.Object{ - Name: file.GetName(), - Size: file.GetSize(), - Modified: file.ModTime(), - IsFolder: false, - } - return uploadedObj, nil + return d.uploadFile(ctx, dstDir.GetID(), file, up) } func (d *ProtonDrive) GetDetails(ctx context.Context) (*model.StorageDetails, error) { diff --git a/drivers/proton_drive/util.go b/drivers/proton_drive/util.go index 43874c172..3426cfb2e 100644 --- a/drivers/proton_drive/util.go +++ b/drivers/proton_drive/util.go @@ -74,10 +74,10 @@ func (d *ProtonDrive) searchByPath(ctx context.Context, fullPath string, isFolde return currentLink, nil } -func (d *ProtonDrive) uploadFile(ctx context.Context, parentLinkID string, file model.FileStreamer, up driver.UpdateProgress) error { +func (d *ProtonDrive) uploadFile(ctx context.Context, parentLinkID string, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { _, err := d.protonDrive.GetLink(ctx, parentLinkID) if err != nil { - return fmt.Errorf("failed to get parent link: %w", err) + return nil, fmt.Errorf("failed to get parent link: %w", err) } var reader io.Reader @@ -105,12 +105,18 @@ func (d *ProtonDrive) uploadFile(ctx context.Context, parentLinkID string, file } reader = driver.NewLimitedUploadStream(ctx, reader) - _, _, err = d.protonDrive.UploadFileByReader(ctx, parentLinkID, file.GetName(), file.ModTime(), reader, 0) + id, _, err := d.protonDrive.UploadFileByReader(ctx, parentLinkID, file.GetName(), file.ModTime(), reader, 0) if err != nil { - return fmt.Errorf("failed to upload file: %w", err) + return nil, fmt.Errorf("failed to upload file: %w", err) } - return nil + return &model.Object{ + ID: id, + Name: file.GetName(), + Size: file.GetSize(), + Modified: file.ModTime(), + IsFolder: false, + }, nil } func (d *ProtonDrive) ensureTempServer() error { From 493b96cc2da2440415a13dd2a480dd0fbf94dddb Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Sat, 11 Oct 2025 20:14:52 +0800 Subject: [PATCH 14/26] fix(proton_drive): refactor DirectMove to use getLink for source retrieval and simplify destination handling --- drivers/proton_drive/driver.go | 4 ++-- drivers/proton_drive/util.go | 18 +++++------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index 0f6cdfd75..db617cc3d 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -181,7 +181,7 @@ func (d *ProtonDrive) List(ctx context.Context, dir model.Obj, args model.ListAr } func (d *ProtonDrive) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { - link, err := d.protonDrive.GetLink(ctx, file.GetID()) + link, err := d.getLink(ctx, file.GetID()) if err != nil { return nil, fmt.Errorf("failed get file link: %+v", err) } @@ -249,7 +249,7 @@ func (d *ProtonDrive) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model return nil, fmt.Errorf("directory copy not supported") } - srcLink, err := d.searchByPath(ctx, srcObj.GetPath(), false) + srcLink, err := d.getLink(ctx, srcObj.GetID()) if err != nil { return nil, err } diff --git a/drivers/proton_drive/util.go b/drivers/proton_drive/util.go index 3426cfb2e..d961a20ae 100644 --- a/drivers/proton_drive/util.go +++ b/drivers/proton_drive/util.go @@ -75,7 +75,7 @@ func (d *ProtonDrive) searchByPath(ctx context.Context, fullPath string, isFolde } func (d *ProtonDrive) uploadFile(ctx context.Context, parentLinkID string, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { - _, err := d.protonDrive.GetLink(ctx, parentLinkID) + _, err := d.getLink(ctx, parentLinkID) if err != nil { return nil, fmt.Errorf("failed to get parent link: %w", err) } @@ -590,7 +590,7 @@ func (d *ProtonDrive) DirectRename(ctx context.Context, srcObj model.Obj, newNam return nil, fmt.Errorf("protonDrive bridge is nil") } - srcLink, err := d.protonDrive.GetLink(ctx, srcObj.GetID()) + srcLink, err := d.getLink(ctx, srcObj.GetID()) if err != nil { return nil, fmt.Errorf("failed to find source: %w", err) } @@ -743,21 +743,12 @@ func (d *ProtonDrive) executeMoveAPI(ctx context.Context, linkID string, req Mov func (d *ProtonDrive) DirectMove(ctx context.Context, srcObj model.Obj, dstDir model.Obj) (model.Obj, error) { // fmt.Printf("DEBUG DirectMove: srcPath=%s, dstPath=%s", srcObj.GetPath(), dstDir.GetPath()) - srcLink, err := d.searchByPath(ctx, srcObj.GetPath(), srcObj.IsDir()) + srcLink, err := d.getLink(ctx, srcObj.GetID()) if err != nil { return nil, fmt.Errorf("failed to find source: %w", err) } - var dstParentLinkID string - if dstDir.GetPath() == "/" { - dstParentLinkID = d.RootFolderID - } else { - dstLink, err := d.searchByPath(ctx, dstDir.GetPath(), true) - if err != nil { - return nil, fmt.Errorf("failed to find destination: %w", err) - } - dstParentLinkID = dstLink.LinkID - } + dstParentLinkID := dstDir.GetID() if srcObj.IsDir() { // Check if destination is a descendant of source @@ -815,6 +806,7 @@ func (d *ProtonDrive) DirectMove(ctx context.Context, srcObj model.Obj, dstDir m } return &model.Object{ + ID: srcLink.LinkID, Name: srcObj.GetName(), Size: srcObj.GetSize(), Modified: srcObj.ModTime(), From 404879f6d060ee4f120dd258973ddc73053d51cb Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Sat, 11 Oct 2025 20:32:19 +0800 Subject: [PATCH 15/26] fix(proton_drive): simplify Copy method by removing temporary file creation and directly using FileStream --- drivers/proton_drive/driver.go | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index db617cc3d..ca46aec3d 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -265,25 +265,18 @@ func (d *ProtonDrive) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model actualSize = fileSystemAttrs.Size } - tempFile, err := utils.CreateTempFile(reader, actualSize) - if err != nil { - return nil, fmt.Errorf("failed to create temp file: %w", err) - } - defer tempFile.Close() - - updatedObj := &model.Object{ - Name: srcObj.GetName(), - // Use the accurate and real size - Size: actualSize, - Modified: srcObj.ModTime(), - IsFolder: false, + file := &stream.FileStream{ + Ctx: ctx, + Obj: &model.Object{ + Name: srcObj.GetName(), + // Use the accurate and real size + Size: actualSize, + Modified: srcObj.ModTime(), + }, + Reader: reader, } - - return d.Put(ctx, dstDir, &stream.FileStream{ - Ctx: ctx, - Obj: updatedObj, - Reader: tempFile, - }, nil) + defer file.Close() + return d.Put(ctx, dstDir, file, func(percentage float64) {}) } func (d *ProtonDrive) Remove(ctx context.Context, obj model.Obj) error { From 2e36284ee8b87d9e7a06ee50bfa90d3c67e22084 Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Sat, 11 Oct 2025 20:36:31 +0800 Subject: [PATCH 16/26] refactor(proton_drive): remove unused temporary server and related code --- drivers/proton_drive/driver.go | 10 -- drivers/proton_drive/meta.go | 14 +- drivers/proton_drive/types.go | 45 ----- drivers/proton_drive/util.go | 293 --------------------------------- 4 files changed, 5 insertions(+), 357 deletions(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index ca46aec3d..fc2ce1886 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -20,8 +20,6 @@ import ( "encoding/base64" "fmt" "io" - "net/http" - "sync" "time" "github.com/OpenListTeam/OpenList/v4/internal/driver" @@ -48,12 +46,7 @@ type ProtonDrive struct { sdkVersion string webDriveAV string - tempServer *http.Server - downloadTokens map[string]*downloadInfo - tokenMutex sync.RWMutex - c *proton.Client - // m *proton.Manager // userKR *crypto.KeyRing addrKRs map[string]*crypto.KeyRing @@ -153,9 +146,6 @@ func (d *ProtonDrive) Init(ctx context.Context) error { } func (d *ProtonDrive) Drop(ctx context.Context) error { - if d.tempServer != nil { - d.tempServer.Shutdown(ctx) - } return nil } diff --git a/drivers/proton_drive/meta.go b/drivers/proton_drive/meta.go index 2521db166..acf656779 100644 --- a/drivers/proton_drive/meta.go +++ b/drivers/proton_drive/meta.go @@ -23,15 +23,11 @@ import ( 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"` - TempServerListenPort int `json:"temp_server_listen_port" type:"number" default:"0" help:"Internal port for temp server to bind to (0 for auto, preferred = 8080)"` - TempServerPublicPort int `json:"temp_server_public_port" type:"number" default:"0" help:"External port that clients will connect to (0 for auto, preferred = 8080)"` - TempServerPublicHost string `json:"temp_server_public_host" type:"string" default:"127.0.0.1" help:"External domain/IP that clients will connect to i.e. 192.168.1.5 (default = 127.0.0.1)"` - + 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 } diff --git a/drivers/proton_drive/types.go b/drivers/proton_drive/types.go index 6313cfd71..ed6dcd192 100644 --- a/drivers/proton_drive/types.go +++ b/drivers/proton_drive/types.go @@ -15,51 +15,6 @@ D@' 3z K!7 - The King Of Cracking Да здравствует Родина)) */ -import ( - "time" - - "github.com/henrybear327/go-proton-api" -) - -type ProtonFile struct { - *proton.Link - Name string - IsFolder bool -} - -func (p *ProtonFile) GetName() string { - return p.Name -} - -func (p *ProtonFile) GetSize() int64 { - return p.Link.Size -} - -func (p *ProtonFile) GetPath() string { - return p.Name -} - -func (p *ProtonFile) IsDir() bool { - return p.IsFolder -} - -func (p *ProtonFile) ModTime() time.Time { - return time.Unix(p.Link.ModifyTime, 0) -} - -func (p *ProtonFile) CreateTime() time.Time { - return time.Unix(p.Link.CreateTime, 0) -} - -type downloadInfo struct { - LinkID string - FileName string -} - -type httpRange struct { - start, end int64 -} - type MoveRequest struct { ParentLinkID string `json:"ParentLinkID"` NodePassphrase string `json:"NodePassphrase"` diff --git a/drivers/proton_drive/util.go b/drivers/proton_drive/util.go index d961a20ae..5e31b55af 100644 --- a/drivers/proton_drive/util.go +++ b/drivers/proton_drive/util.go @@ -23,13 +23,7 @@ import ( "errors" "fmt" "io" - "mime" - "net" "net/http" - "path/filepath" - "strconv" - "strings" - "time" "github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/model" @@ -37,43 +31,6 @@ import ( "github.com/henrybear327/go-proton-api" ) -func (d *ProtonDrive) searchByPath(ctx context.Context, fullPath string, isFolder bool) (*proton.Link, error) { - if fullPath == "/" { - return d.protonDrive.RootLink, nil - } - - cleanPath := strings.Trim(fullPath, "/") - pathParts := strings.Split(cleanPath, "/") - - currentLink := d.protonDrive.RootLink - - for i, part := range pathParts { - isLastPart := i == len(pathParts)-1 - searchForFolder := !isLastPart || isFolder - - entries, err := d.protonDrive.ListDirectory(ctx, currentLink.LinkID) - if err != nil { - return nil, fmt.Errorf("failed to list directory: %w", err) - } - - found := false - for _, entry := range entries { - // entry.Name is already decrypted! - if entry.Name == part && entry.IsFolder == searchForFolder { - currentLink = entry.Link - found = true - break - } - } - - if !found { - return nil, fmt.Errorf("path not found: %s (looking for part: %s)", fullPath, part) - } - } - - return currentLink, nil -} - func (d *ProtonDrive) uploadFile(ctx context.Context, parentLinkID string, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) { _, err := d.getLink(ctx, parentLinkID) if err != nil { @@ -119,256 +76,6 @@ func (d *ProtonDrive) uploadFile(ctx context.Context, parentLinkID string, file }, nil } -func (d *ProtonDrive) ensureTempServer() error { - if d.tempServer != nil { - // Already running - return nil - } - - // Use configured listen port, or auto-assign if 0 - if d.TempServerListenPort == 0 { - // Try preferred port 8080 first - listener, err := net.Listen("tcp", ":8080") - if err == nil { - // Port 8080 is available (default) - d.TempServerListenPort = 8080 - listener.Close() - } else { - // Port 8080 not available, auto-assign any available port - listener, err := net.Listen("tcp", ":0") - if err != nil { - return fmt.Errorf("failed to get available port: %w", err) - } - d.TempServerListenPort = listener.Addr().(*net.TCPAddr).Port - listener.Close() - } - } - - //If public port is default, use the same port as listen - if d.TempServerPublicPort == 0 { - d.TempServerPublicPort = d.TempServerListenPort - } else { - // Validate that the configured public port is available - testListener, err := net.Listen("tcp", fmt.Sprintf(":%d", d.TempServerPublicPort)) - if err != nil { - // Public port not available, fall back to listen port - fmt.Printf("ProtonDrive: configured public port %d not available, falling back to listen port %d\n", - d.TempServerPublicPort, d.TempServerListenPort) - d.TempServerPublicPort = d.TempServerListenPort - - fmt.Printf("ProtonDrive: if using container like Docker, NAT env or VM \nplease configure external port forwarding/mapping %d -> %d \n", d.TempServerListenPort, d.TempServerPublicPort) - } else { - // Port is available, close the listener - testListener.Close() - } - } - - listener, err := net.Listen("tcp", fmt.Sprintf(":%d", d.TempServerListenPort)) - if err != nil { - return fmt.Errorf("failed to listen on port %d: %w", d.TempServerListenPort, err) - } - - //fmt.Printf("d.TempServerListenPort: %d\n", d.TempServerListenPort) - - mux := http.NewServeMux() - mux.HandleFunc("/temp/", d.handleTempDownload) - - d.tempServer = &http.Server{ - Handler: mux, - } - - go func() { - if err := d.tempServer.Serve(listener); err != nil && err != http.ErrServerClosed { - fmt.Printf("ProtonDrive temp server error: %v\n", err) - } - }() - - return nil -} - -func (d *ProtonDrive) handleTempDownload(w http.ResponseWriter, r *http.Request) { - token := strings.TrimPrefix(r.URL.Path, "/temp/") - - d.tokenMutex.RLock() - info, exists := d.downloadTokens[token] - d.tokenMutex.RUnlock() - - if !exists { - http.Error(w, "Invalid or expired token", http.StatusNotFound) - return - } - - link, err := d.protonDrive.GetLink(r.Context(), info.LinkID) - if err != nil { - http.Error(w, "Failed to get file link", http.StatusInternalServerError) - return - } - - // Get file size for range calculations - _, _, attrs, err := d.protonDrive.DownloadFile(r.Context(), link, 0) - if err != nil { - http.Error(w, "Failed to get file info", http.StatusInternalServerError) - return - } - - fileSize := attrs.Size - - rangeHeader := r.Header.Get("Range") - if rangeHeader != "" { - - // Parse range header like "bytes=0-1023" or "bytes=1024-" - ranges, err := parseRange(rangeHeader, fileSize) - if err != nil { - http.Error(w, "Invalid range", http.StatusRequestedRangeNotSatisfiable) - return - } - - if len(ranges) == 1 { - - // Single range request, small - start, end := ranges[0].start, ranges[0].end - contentLength := end - start + 1 - - // Start download from offset - reader, _, _, err := d.protonDrive.DownloadFile(r.Context(), link, start) - if err != nil { - http.Error(w, "Failed to start download", http.StatusInternalServerError) - return - } - defer reader.Close() - - w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fileSize)) - w.Header().Set("Content-Length", fmt.Sprintf("%d", contentLength)) - w.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(link.Name))) - - // Partial content... - // Setting fileName is more cosmetical here - //.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", link.Name)) - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", info.FileName)) - w.Header().Set("Accept-Ranges", "bytes") - - w.WriteHeader(http.StatusPartialContent) - - io.CopyN(w, reader, contentLength) - return - } - } - - // Full file download (non-range request) - reader, _, _, err := d.protonDrive.DownloadFile(r.Context(), link, 0) - if err != nil { - http.Error(w, "Failed to start download", http.StatusInternalServerError) - return - } - defer reader.Close() - - // Set headers for full content - w.Header().Set("Content-Length", fmt.Sprintf("%d", fileSize)) - w.Header().Set("Content-Type", mime.TypeByExtension(filepath.Ext(link.Name))) - - // Setting fileName is needed since ProtonDrive fileName is more like a random string - // w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", link.Name)) - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", info.FileName)) - - w.Header().Set("Accept-Ranges", "bytes") - - // Stream the full file - io.Copy(w, reader) -} - -func (d *ProtonDrive) generateDownloadToken(linkID, fileName string) string { - token := fmt.Sprintf("%d_%s", time.Now().UnixNano(), linkID[:8]) - - d.tokenMutex.Lock() - if d.downloadTokens == nil { - d.downloadTokens = make(map[string]*downloadInfo) - } - - d.downloadTokens[token] = &downloadInfo{ - LinkID: linkID, - FileName: fileName, - } - - d.tokenMutex.Unlock() - - go func() { - // Token expires in 1 hour - time.Sleep(1 * time.Hour) - d.tokenMutex.Lock() - - delete(d.downloadTokens, token) - d.tokenMutex.Unlock() - }() - - return token -} - -func parseRange(rangeHeader string, size int64) ([]httpRange, error) { - if !strings.HasPrefix(rangeHeader, "bytes=") { - return nil, fmt.Errorf("invalid range header") - } - - rangeSpec := strings.TrimPrefix(rangeHeader, "bytes=") - ranges := strings.Split(rangeSpec, ",") - - var result []httpRange - for _, r := range ranges { - r = strings.TrimSpace(r) - if strings.Contains(r, "-") { - parts := strings.Split(r, "-") - if len(parts) != 2 { - return nil, fmt.Errorf("invalid range format") - } - - var start, end int64 - var err error - - if parts[0] == "" { - - // Suffix range (e.g., "-500") - if parts[1] == "" { - return nil, fmt.Errorf("invalid range format") - } - end = size - 1 - start, err = strconv.ParseInt(parts[1], 10, 64) - if err != nil { - return nil, err - } - start = size - start - if start < 0 { - start = 0 - } - } else if parts[1] == "" { - - // Prefix range (e.g., "500-") - start, err = strconv.ParseInt(parts[0], 10, 64) - if err != nil { - return nil, err - } - end = size - 1 - } else { - // Full range (e.g., "0-1023") - start, err = strconv.ParseInt(parts[0], 10, 64) - if err != nil { - return nil, err - } - end, err = strconv.ParseInt(parts[1], 10, 64) - if err != nil { - return nil, err - } - } - - if start >= size || end >= size || start > end { - return nil, fmt.Errorf("range out of bounds") - } - - result = append(result, httpRange{start: start, end: end}) - } - } - - return result, nil -} - func (d *ProtonDrive) encryptFileName(ctx context.Context, name string, parentLinkID string) (string, error) { parentLink, err := d.getLink(ctx, parentLinkID) if err != nil { From f81cc1b29b9c52e404ab12dc03430e184eb78eef Mon Sep 17 00:00:00 2001 From: KirCute Date: Sat, 11 Oct 2025 21:18:03 +0800 Subject: [PATCH 17/26] chore --- drivers/proton_drive/driver.go | 6 +++--- drivers/proton_drive/util.go | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index fc2ce1886..707a3159c 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -66,10 +66,10 @@ func (d *ProtonDrive) GetAddition() driver.Additional { return &d.Addition } -func (d *ProtonDrive) Init(ctx context.Context) error { +func (d *ProtonDrive) Init(ctx context.Context) (err error) { defer func() { - if r := recover(); r != nil { - fmt.Printf("ProtonDrive initialization panic: %v", r) + if r := recover(); err == nil && r != nil { + err = fmt.Errorf("ProtonDrive initialization panic: %v", r) } }() diff --git a/drivers/proton_drive/util.go b/drivers/proton_drive/util.go index 5e31b55af..8a3efdef8 100644 --- a/drivers/proton_drive/util.go +++ b/drivers/proton_drive/util.go @@ -27,6 +27,7 @@ import ( "github.com/OpenListTeam/OpenList/v4/internal/driver" "github.com/OpenListTeam/OpenList/v4/internal/model" + "github.com/OpenListTeam/OpenList/v4/internal/stream" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/henrybear327/go-proton-api" ) @@ -57,7 +58,10 @@ func (d *ProtonDrive) uploadFile(ctx context.Context, parentLinkID string, file // reader = bufio.NewReader(file) reader = bufio.NewReaderSize(file, bufferSize) reader = &driver.ReaderUpdatingProgress{ - Reader: file, + Reader: &stream.SimpleReaderWithSize{ + Reader: reader, + Size: file.GetSize(), + }, UpdateProgress: up, } reader = driver.NewLimitedUploadStream(ctx, reader) From 8868cf7aaf79383cc871d6aa50d0ce899c0c4d77 Mon Sep 17 00:00:00 2001 From: Da3zKi7 Date: Sun, 12 Oct 2025 21:59:33 -0600 Subject: [PATCH 18/26] fix(proton_drive): fix driver - Handle fresh login if ProtonDrive rejects AccessToken or RefreshToken - Update stored credentials --- drivers/proton_drive/driver.go | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index 707a3159c..50df32210 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -24,6 +24,7 @@ import ( "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/stream" "github.com/OpenListTeam/OpenList/v4/pkg/http_range" "github.com/OpenListTeam/OpenList/v4/pkg/utils" @@ -104,17 +105,37 @@ func (d *ProtonDrive) Init(ctx context.Context) (err error) { ReusableCredential: reusableCredential, } - protonDrive, _, err := proton_api_bridge.NewProtonDrive( + protonDrive, updatedCredentials, err := proton_api_bridge.NewProtonDrive( ctx, config, func(auth proton.Auth) {}, func() {}, ) + if err != nil && useReusableLogin { + config.UseReusableLogin = false + protonDrive, updatedCredentials, err = proton_api_bridge.NewProtonDrive(ctx, + config, + func(auth proton.Auth) {}, + func() {}, + ) + } + if err != nil { return fmt.Errorf("failed to initialize ProtonDrive: %w", err) } + if updatedCredentials != nil { + reusableCredential = &common.ReusableCredentialData{ + UID: updatedCredentials.UID, + AccessToken: updatedCredentials.AccessToken, + RefreshToken: updatedCredentials.RefreshToken, + SaltedKeyPass: updatedCredentials.SaltedKeyPass, + } + d.ReusableCredential = *reusableCredential + op.MustSaveDriverStorage(d) + } + clientOptions := []proton.Option{ proton.WithAppVersion(d.appVersion), proton.WithUserAgent(d.userAgent), From 9341c8ba4faa0060b058cee759fc35efd32d3814 Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Mon, 13 Oct 2025 16:11:55 +0800 Subject: [PATCH 19/26] fix(proton_drive): simplify reusable login handling in Init method --- drivers/proton_drive/driver.go | 36 ++++++++++------------------------ 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index 50df32210..8a8c75301 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -81,14 +81,6 @@ func (d *ProtonDrive) Init(ctx context.Context) (err error) { return fmt.Errorf("password is required") } - useReusableLogin := false - reusableCredential := &d.ReusableCredential - - if d.UseReusableLogin && reusableCredential.UID != "" && reusableCredential.AccessToken != "" && - reusableCredential.RefreshToken != "" && reusableCredential.SaltedKeyPass != "" { - useReusableLogin = true - } - config := &common.Config{ AppVersion: d.appVersion, UserAgent: d.userAgent, @@ -100,50 +92,42 @@ func (d *ProtonDrive) Init(ctx context.Context) (err error) { EnableCaching: true, ConcurrentBlockUploadCount: 5, ConcurrentFileCryptoCount: 2, - UseReusableLogin: useReusableLogin, + UseReusableLogin: d.UseReusableLogin && d.ReusableCredential != (common.ReusableCredentialData{}), ReplaceExistingDraft: true, - ReusableCredential: reusableCredential, + ReusableCredential: &d.ReusableCredential, } - protonDrive, updatedCredentials, err := proton_api_bridge.NewProtonDrive( + protonDrive, _, err := proton_api_bridge.NewProtonDrive( ctx, config, func(auth proton.Auth) {}, func() {}, ) - if err != nil && useReusableLogin { + if err != nil && config.UseReusableLogin { config.UseReusableLogin = false - protonDrive, updatedCredentials, err = proton_api_bridge.NewProtonDrive(ctx, + protonDrive, _, err = proton_api_bridge.NewProtonDrive(ctx, config, func(auth proton.Auth) {}, func() {}, ) + if err == nil { + op.MustSaveDriverStorage(d) + } } if err != nil { return fmt.Errorf("failed to initialize ProtonDrive: %w", err) } - if updatedCredentials != nil { - reusableCredential = &common.ReusableCredentialData{ - UID: updatedCredentials.UID, - AccessToken: updatedCredentials.AccessToken, - RefreshToken: updatedCredentials.RefreshToken, - SaltedKeyPass: updatedCredentials.SaltedKeyPass, - } - d.ReusableCredential = *reusableCredential - op.MustSaveDriverStorage(d) - } - clientOptions := []proton.Option{ proton.WithAppVersion(d.appVersion), proton.WithUserAgent(d.userAgent), } manager := proton.New(clientOptions...) - d.c = manager.NewClient(reusableCredential.UID, reusableCredential.AccessToken, reusableCredential.RefreshToken) + d.c = manager.NewClient(d.ReusableCredential.UID, d.ReusableCredential.AccessToken, d.ReusableCredential.RefreshToken) - saltedKeyPassBytes, err := base64.StdEncoding.DecodeString(reusableCredential.SaltedKeyPass) + saltedKeyPassBytes, err := base64.StdEncoding.DecodeString(d.ReusableCredential.SaltedKeyPass) if err != nil { return fmt.Errorf("failed to decode salted key pass: %w", err) } From 2fa6b05b80c16d8e7f36380acad3b737aba6a182 Mon Sep 17 00:00:00 2001 From: Da3zKi7 Date: Tue, 14 Oct 2025 09:18:27 -0600 Subject: [PATCH 20/26] fix(proton_drive): fix driver - Update stored credentials, now is failing --- drivers/proton_drive/driver.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index 8a8c75301..f4c7fbb69 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -97,7 +97,7 @@ func (d *ProtonDrive) Init(ctx context.Context) (err error) { ReusableCredential: &d.ReusableCredential, } - protonDrive, _, err := proton_api_bridge.NewProtonDrive( + protonDrive, updatedCredentials, err := proton_api_bridge.NewProtonDrive( ctx, config, func(auth proton.Auth) {}, @@ -105,21 +105,29 @@ func (d *ProtonDrive) Init(ctx context.Context) (err error) { ) if err != nil && config.UseReusableLogin { + fmt.Printf("ProtonDrive: Reusable login failed, falling back to fresh login: %v\n", err) config.UseReusableLogin = false - protonDrive, _, err = proton_api_bridge.NewProtonDrive(ctx, + protonDrive, updatedCredentials, err = proton_api_bridge.NewProtonDrive(ctx, config, func(auth proton.Auth) {}, func() {}, ) - if err == nil { - op.MustSaveDriverStorage(d) - } } if err != nil { return fmt.Errorf("failed to initialize ProtonDrive: %w", err) } + if updatedCredentials != nil { + d.ReusableCredential = common.ReusableCredentialData{ + UID: updatedCredentials.UID, + AccessToken: updatedCredentials.AccessToken, + RefreshToken: updatedCredentials.RefreshToken, + SaltedKeyPass: updatedCredentials.SaltedKeyPass, + } + op.MustSaveDriverStorage(d) + } + clientOptions := []proton.Option{ proton.WithAppVersion(d.appVersion), proton.WithUserAgent(d.userAgent), From ce453879169c38bdb5da7036da2c0f0fd21e0cf5 Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Wed, 15 Oct 2025 00:08:23 +0800 Subject: [PATCH 21/26] feat(proton_drive): improve authentication handling and remove unused variables --- drivers/proton_drive/driver.go | 22 +++++++--------------- drivers/proton_drive/util.go | 9 +++++++++ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index f4c7fbb69..cdf1a4653 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -97,37 +97,29 @@ func (d *ProtonDrive) Init(ctx context.Context) (err error) { ReusableCredential: &d.ReusableCredential, } - protonDrive, updatedCredentials, err := proton_api_bridge.NewProtonDrive( + protonDrive, _, err := proton_api_bridge.NewProtonDrive( ctx, config, - func(auth proton.Auth) {}, + d.authHandler, func() {}, ) if err != nil && config.UseReusableLogin { - fmt.Printf("ProtonDrive: Reusable login failed, falling back to fresh login: %v\n", err) config.UseReusableLogin = false - protonDrive, updatedCredentials, err = proton_api_bridge.NewProtonDrive(ctx, + protonDrive, _, err = proton_api_bridge.NewProtonDrive(ctx, config, - func(auth proton.Auth) {}, + d.authHandler, func() {}, ) + if err == nil { + op.MustSaveDriverStorage(d) + } } if err != nil { return fmt.Errorf("failed to initialize ProtonDrive: %w", err) } - if updatedCredentials != nil { - d.ReusableCredential = common.ReusableCredentialData{ - UID: updatedCredentials.UID, - AccessToken: updatedCredentials.AccessToken, - RefreshToken: updatedCredentials.RefreshToken, - SaltedKeyPass: updatedCredentials.SaltedKeyPass, - } - op.MustSaveDriverStorage(d) - } - clientOptions := []proton.Option{ proton.WithAppVersion(d.appVersion), proton.WithUserAgent(d.userAgent), diff --git a/drivers/proton_drive/util.go b/drivers/proton_drive/util.go index 8a3efdef8..97a613d31 100644 --- a/drivers/proton_drive/util.go +++ b/drivers/proton_drive/util.go @@ -27,6 +27,7 @@ import ( "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/stream" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/henrybear327/go-proton-api" @@ -628,3 +629,11 @@ func (d *ProtonDrive) checkCircularMove(ctx context.Context, srcLinkID, dstParen return nil } + +func (d *ProtonDrive) authHandler(auth proton.Auth) { + if auth.AccessToken != d.ReusableCredential.AccessToken || auth.RefreshToken != d.ReusableCredential.RefreshToken { + d.ReusableCredential.AccessToken = auth.AccessToken + d.ReusableCredential.RefreshToken = auth.RefreshToken + op.MustSaveDriverStorage(d) + } +} From 855aab176ec01550726fa0d6ff5376cb0a3cb835 Mon Sep 17 00:00:00 2001 From: Da3zKi7 Date: Tue, 14 Oct 2025 10:18:29 -0600 Subject: [PATCH 22/26] fix(proton_drive): fix driver - Update stored credentials, now is failing --- drivers/proton_drive/driver.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index cdf1a4653..f4c7fbb69 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -97,29 +97,37 @@ func (d *ProtonDrive) Init(ctx context.Context) (err error) { ReusableCredential: &d.ReusableCredential, } - protonDrive, _, err := proton_api_bridge.NewProtonDrive( + protonDrive, updatedCredentials, err := proton_api_bridge.NewProtonDrive( ctx, config, - d.authHandler, + func(auth proton.Auth) {}, func() {}, ) if err != nil && config.UseReusableLogin { + fmt.Printf("ProtonDrive: Reusable login failed, falling back to fresh login: %v\n", err) config.UseReusableLogin = false - protonDrive, _, err = proton_api_bridge.NewProtonDrive(ctx, + protonDrive, updatedCredentials, err = proton_api_bridge.NewProtonDrive(ctx, config, - d.authHandler, + func(auth proton.Auth) {}, func() {}, ) - if err == nil { - op.MustSaveDriverStorage(d) - } } if err != nil { return fmt.Errorf("failed to initialize ProtonDrive: %w", err) } + if updatedCredentials != nil { + d.ReusableCredential = common.ReusableCredentialData{ + UID: updatedCredentials.UID, + AccessToken: updatedCredentials.AccessToken, + RefreshToken: updatedCredentials.RefreshToken, + SaltedKeyPass: updatedCredentials.SaltedKeyPass, + } + op.MustSaveDriverStorage(d) + } + clientOptions := []proton.Option{ proton.WithAppVersion(d.appVersion), proton.WithUserAgent(d.userAgent), From 554dad09f1811263677d5eac43a514341f9f8f3e Mon Sep 17 00:00:00 2001 From: Da3zKi7 Date: Tue, 14 Oct 2025 10:25:13 -0600 Subject: [PATCH 23/26] fix(proton_drive): improve authentication handling --- drivers/proton_drive/driver.go | 22 +++++++--------------- drivers/proton_drive/util.go | 1 + 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index f4c7fbb69..cdf1a4653 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -97,37 +97,29 @@ func (d *ProtonDrive) Init(ctx context.Context) (err error) { ReusableCredential: &d.ReusableCredential, } - protonDrive, updatedCredentials, err := proton_api_bridge.NewProtonDrive( + protonDrive, _, err := proton_api_bridge.NewProtonDrive( ctx, config, - func(auth proton.Auth) {}, + d.authHandler, func() {}, ) if err != nil && config.UseReusableLogin { - fmt.Printf("ProtonDrive: Reusable login failed, falling back to fresh login: %v\n", err) config.UseReusableLogin = false - protonDrive, updatedCredentials, err = proton_api_bridge.NewProtonDrive(ctx, + protonDrive, _, err = proton_api_bridge.NewProtonDrive(ctx, config, - func(auth proton.Auth) {}, + d.authHandler, func() {}, ) + if err == nil { + op.MustSaveDriverStorage(d) + } } if err != nil { return fmt.Errorf("failed to initialize ProtonDrive: %w", err) } - if updatedCredentials != nil { - d.ReusableCredential = common.ReusableCredentialData{ - UID: updatedCredentials.UID, - AccessToken: updatedCredentials.AccessToken, - RefreshToken: updatedCredentials.RefreshToken, - SaltedKeyPass: updatedCredentials.SaltedKeyPass, - } - op.MustSaveDriverStorage(d) - } - clientOptions := []proton.Option{ proton.WithAppVersion(d.appVersion), proton.WithUserAgent(d.userAgent), diff --git a/drivers/proton_drive/util.go b/drivers/proton_drive/util.go index 97a613d31..a75006852 100644 --- a/drivers/proton_drive/util.go +++ b/drivers/proton_drive/util.go @@ -632,6 +632,7 @@ func (d *ProtonDrive) checkCircularMove(ctx context.Context, srcLinkID, dstParen func (d *ProtonDrive) authHandler(auth proton.Auth) { if auth.AccessToken != d.ReusableCredential.AccessToken || auth.RefreshToken != d.ReusableCredential.RefreshToken { + d.ReusableCredential.UID = auth.UID d.ReusableCredential.AccessToken = auth.AccessToken d.ReusableCredential.RefreshToken = auth.RefreshToken op.MustSaveDriverStorage(d) From f29c58d8283691a25b186716b71013870ac03d85 Mon Sep 17 00:00:00 2001 From: j2rong4cn Date: Wed, 15 Oct 2025 01:01:01 +0800 Subject: [PATCH 24/26] refactor(proton_drive): move client initialization to initClient method --- drivers/proton_drive/driver.go | 8 +------- drivers/proton_drive/util.go | 10 ++++++++++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index cdf1a4653..0bcccc4a2 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -120,18 +120,12 @@ func (d *ProtonDrive) Init(ctx context.Context) (err error) { return fmt.Errorf("failed to initialize ProtonDrive: %w", err) } - clientOptions := []proton.Option{ - proton.WithAppVersion(d.appVersion), - proton.WithUserAgent(d.userAgent), - } - manager := proton.New(clientOptions...) - d.c = manager.NewClient(d.ReusableCredential.UID, d.ReusableCredential.AccessToken, d.ReusableCredential.RefreshToken) - saltedKeyPassBytes, err := base64.StdEncoding.DecodeString(d.ReusableCredential.SaltedKeyPass) if err != nil { return fmt.Errorf("failed to decode salted key pass: %w", err) } + d.initClient() _, addrKRs, addrs, _, err := getAccountKRs(ctx, d.c, nil, saltedKeyPassBytes) if err != nil { return fmt.Errorf("failed to get account keyrings: %w", err) diff --git a/drivers/proton_drive/util.go b/drivers/proton_drive/util.go index a75006852..149a0b347 100644 --- a/drivers/proton_drive/util.go +++ b/drivers/proton_drive/util.go @@ -635,6 +635,16 @@ func (d *ProtonDrive) authHandler(auth proton.Auth) { d.ReusableCredential.UID = auth.UID d.ReusableCredential.AccessToken = auth.AccessToken d.ReusableCredential.RefreshToken = auth.RefreshToken + d.initClient() op.MustSaveDriverStorage(d) } } + +func (d *ProtonDrive) initClient() { + clientOptions := []proton.Option{ + proton.WithAppVersion(d.appVersion), + proton.WithUserAgent(d.userAgent), + } + manager := proton.New(clientOptions...) + d.c = manager.NewClient(d.ReusableCredential.UID, d.ReusableCredential.AccessToken, d.ReusableCredential.RefreshToken) +} From 0b84a1cfe6f17f76d958243a3adfe6ad391bc301 Mon Sep 17 00:00:00 2001 From: Da3zKi7 Date: Wed, 15 Oct 2025 10:19:31 -0600 Subject: [PATCH 25/26] feat(proton_drive): move addrs and addrKRs --- drivers/proton_drive/driver.go | 14 ++------------ drivers/proton_drive/util.go | 24 ++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index 0bcccc4a2..9d8cf6dc0 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -17,7 +17,6 @@ D@' 3z K!7 - The King Of Cracking import ( "context" - "encoding/base64" "fmt" "io" "time" @@ -120,15 +119,8 @@ func (d *ProtonDrive) Init(ctx context.Context) (err error) { return fmt.Errorf("failed to initialize ProtonDrive: %w", err) } - saltedKeyPassBytes, err := base64.StdEncoding.DecodeString(d.ReusableCredential.SaltedKeyPass) - if err != nil { - return fmt.Errorf("failed to decode salted key pass: %w", err) - } - - d.initClient() - _, addrKRs, addrs, _, err := getAccountKRs(ctx, d.c, nil, saltedKeyPassBytes) - if err != nil { - return fmt.Errorf("failed to get account keyrings: %w", err) + if err := d.initClient(ctx); err != nil { + return err } d.protonDrive = protonDrive @@ -138,8 +130,6 @@ func (d *ProtonDrive) Init(ctx context.Context) (err error) { } d.MainShareKR = protonDrive.MainShareKR d.DefaultAddrKR = protonDrive.DefaultAddrKR - d.addrKRs = addrKRs - d.addrData = addrs return nil } diff --git a/drivers/proton_drive/util.go b/drivers/proton_drive/util.go index 149a0b347..a59bb6cb9 100644 --- a/drivers/proton_drive/util.go +++ b/drivers/proton_drive/util.go @@ -19,6 +19,7 @@ import ( "bufio" "bytes" "context" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -635,16 +636,35 @@ func (d *ProtonDrive) authHandler(auth proton.Auth) { d.ReusableCredential.UID = auth.UID d.ReusableCredential.AccessToken = auth.AccessToken d.ReusableCredential.RefreshToken = auth.RefreshToken - d.initClient() + + if err := d.initClient(context.Background()); err != nil { + fmt.Printf("ProtonDrive: failed to reinitialize client after auth refresh: %v\n", err) + } + op.MustSaveDriverStorage(d) } } -func (d *ProtonDrive) initClient() { +func (d *ProtonDrive) initClient(ctx context.Context) error { clientOptions := []proton.Option{ proton.WithAppVersion(d.appVersion), proton.WithUserAgent(d.userAgent), } manager := proton.New(clientOptions...) d.c = manager.NewClient(d.ReusableCredential.UID, d.ReusableCredential.AccessToken, d.ReusableCredential.RefreshToken) + + saltedKeyPassBytes, err := base64.StdEncoding.DecodeString(d.ReusableCredential.SaltedKeyPass) + if err != nil { + return fmt.Errorf("failed to decode salted key pass: %w", err) + } + + _, addrKRs, addrs, _, err := getAccountKRs(ctx, d.c, nil, saltedKeyPassBytes) + if err != nil { + return fmt.Errorf("failed to get account keyrings: %w", err) + } + + d.addrKRs = addrKRs + d.addrData = addrs + + return nil } From fdee792170eb63cf415db05613e7f2d012789d7c Mon Sep 17 00:00:00 2001 From: Da3zKi7 Date: Wed, 15 Oct 2025 10:32:42 -0600 Subject: [PATCH 26/26] feat(proton_drive): optimize upload threads - Change ConcurrentBlockUploadCount to user configured upload threads number - Comment ConcurrentFileCryptoCount, default is runtime.GOMAXPROCS(0) --- drivers/proton_drive/driver.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/drivers/proton_drive/driver.go b/drivers/proton_drive/driver.go index 9d8cf6dc0..06aa645de 100644 --- a/drivers/proton_drive/driver.go +++ b/drivers/proton_drive/driver.go @@ -21,9 +21,11 @@ import ( "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" @@ -89,11 +91,11 @@ func (d *ProtonDrive) Init(ctx context.Context) (err error) { TwoFA: d.TwoFACode, }, EnableCaching: true, - ConcurrentBlockUploadCount: 5, - ConcurrentFileCryptoCount: 2, - UseReusableLogin: d.UseReusableLogin && d.ReusableCredential != (common.ReusableCredentialData{}), - ReplaceExistingDraft: true, - ReusableCredential: &d.ReusableCredential, + 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(