Initial commit

This commit is contained in:
Chun-Hung Tseng
2023-06-20 17:10:43 +02:00
parent 944c82c536
commit 6570484818
25 changed files with 2619 additions and 0 deletions

28
.github/workflows/check.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Lint and Test
on: push
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Get sources
uses: actions/checkout@v3
- name: Set up Go 1.18
uses: actions/setup-go@v3
with:
go-version: '1.18'
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.50.0
args: --timeout=180s
skip-cache: true
# - name: Run tests
# run: go test -v ./...
# - name: Run tests with race check
# run: go test -v -race ./...

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.DS_Store
.credential
.*.credential
data
config.toml

31
Documentation.md Normal file
View File

@@ -0,0 +1,31 @@
# Documentation
Since the Proton API isn't open sourced, this document serves as the team's understanding for future reference.
# Proton Drive API
## Terminology
### Volume
### Share
### Node
### Link
## Encryption
Encryption, decryption, and signature signing and verification, etc. are all performed by using the go-crypto library.
### Login
Proton uses SRP for logging in the users. After logging in, there is a small time window (several minutes) where users can access certain routes, which is in the `scope` field, e.g. getting user salt.
Since the user and address key rings are encrypted with passphrase tied to salt and user password, we need to cache this information as soon as the first log in happens for future usage.
### User Key
### Address Key
### Node/Link Key

135
README.md
View File

@@ -1,2 +1,137 @@
# Proton API Bridge
Thanks to Proton open sourcing [proton-go-api](https://github.com/ProtonMail/go-proton-api) and the web, iOS, and Android client codebases, we don't need to completely reverse engineer the APIs.
[proton-go-api](https://github.com/ProtonMail/go-proton-api) provides the basic building blocks of API calls and error handling, such as 429 exponential back-off, but it is pretty much just a barebone interface to the Proton API. For example, the encryption and decryption of the Proton Drive file are not provided in this library.
This codebase, Proton API Bridge, bridges the gap, so software like [rclone](https://github.com/rclone/rclone) can be built on top of this quickly. This codebase handles the intricate tasks before and after calling Proton APIs, particularly the complex encryption scheme, allowing developers to implement features for other software on top of this codebase.
Currently, only Proton Drive APIs are bridged, as we are aiming to implement a backend for rclone.
## Sidenotes
We are using a fork of the [proton-go-api](https://github.com/henrybear327/go-proton-api), adding quite some new code to it. We will try to commit back to the upstream once we feel like the code changes are stable.
# Instructions to run the code
## Compiling and running
`go run .`
## Unit testing and linting
`golangci-lint run && go test -race -failfast -v ./...`
# Drive APIs
> In collaboration with Azimjon Pulatov, in memory of our good old days at Meta, London, in the summer of 2022.
Currently, the development are split into 2 versions. V1 supports the features [required by rclone](https://github.com/henrybear327/rclone/blob/master/fs/types.go), such as `file listing`. V2 will support the extra features that rclone has interface for, such as `move file` and `move folder` operations.
## V1
### Features
- [x] Log in to an account without 2FA using username and password
- [x] Obtain keyring
- [x] Cache access token, etc. to be able to reuse the session
- [x] Bug: 403: Access token does not have sufficient scope - used the wrong newClient function
- [x] Volume actions
- [x] List all volumes
- [x] Share actions
- [x] Get all shares
- [x] Get default share
- [x] Fix context with proper propagation instead of using `ctx` everywhere
- [x] Folder actions
- [x] List all folders and files within the root folder
- [x] BUG: listing directory - missing signature when there are more than 1 share
- maybe the way I decrypt the keyring is wrong
- (wrong fix for the first time) bug on no name for the root folder
- (correct fix) we need to check for the "active" folder type first
- [x] List all folders and files recursively within the root folder
- [x] Delete
- [x] Implement delete all for testing -> very dangerous, thus currently guarded with a hardcoded string
- [x] Create
- [ ] File actions
- [x] Download
- [x] Download empty file
- [ ] Properly handle large files and empty files (check iOS codebase)
- esp. large files, where buffering in-memory will screw up the runtime
- [ ] Check signature and hash
- [x] Delete
- [x] Upload
- [x] Handle empty file
- [x] Parse mime type
- [ ] Force overwrite
- [ ] Add revision
- [ ] Improve to handle large files
- [ ] Upload verification
- [ ] Handle failed / interrupted upload
- [x] Modified time
- [ ] List file metadata
- [ ] Duplicated file/folder name handling: 422: A file or folder with that name already exists (Code=2500, Status=422)
- [ ] Handle ERROR RESTY 422: File or folder was not found. (Code=2501, Status=422), Attempt 1
- [x] Init ProtonDrive with config passed in as Map
- [x] Remove all `log.Fatalln` and use proper error propagation (basically remove `HandleError` and we go from there)
- [x] Integration tests
- [x] Remove drive demo code
- [x] Create a Drive struct to encapsulate all the functions (maybe?)
- [x] Move comments to proper places
- [x] Modify `shouldRejectDestructiveActions()`
- [ ] Check file metadata
- [ ] Try to check if all functions are used at least once so we know if it's functioning or not
- [ ] Documentation
- [x] Reduce config options on caching access token
- [x] Remove integration test safeguarding
- [ ] Improve file searching function to use HMAC instead of just using string comparison
- [ ] Remove e.g. proton.link related exposures in the function signature (this library should abstract them all)
### TODO
- [x] address go dependencies
- Fixed by doing the following in the `go-proton-api` repo to bump to use the latest commit
- `go get github.com/ProtonMail/go-proton-api@ea8de5f674b7f9b0cca8e3a5076ffe3c5a867e01`
- `go get github.com/ProtonMail/gluon@fb7689b15ae39c3efec3ff3c615c3d2dac41cec8`
- [x] Remove mail-related apis (to reduce dependencies)
- [x] Make a "super class" and expose all necessary methods for the outside to call
- [x] Add 2FA login
- [ ] Go through Drive iOS source code and check the logic control flow
- [x] Fix the function argument passing (using pointers)
- [ ] Use proper AppVersion (we need to be friendly to the Proton servers)
- [ ] Handle account with
- [ ] multiple addresses
- [ ] multiple keys per addresses
- [ ] multiple shares
- [x] Update RClone's contribution.md file
- [x] Remove delete all's hardcoded string
- [ ] Address TODO and FIXME
- [ ] Use CI to run integration tests
- [ ] Figure out the bottleneck by doing some profiling
- [ ] Some error handling from [here](https://github.com/ProtonMail/WebClients/blob/main/packages/shared/lib/drive/constants.ts) MAX_NAME_LENGTH, TIMEOUT
- [x] Point to the right proton-go-api branch
- [x] Run `go get github.com/henrybear327/go-proton-api@dev` to update go mod
### Known limitations
- Large file handling: for uploading, files will be loaded into the memory entirely, encrypted, and then chunked; for downloading, the file will be written when all blocks are decrypted and checked
- Crypto-related operations, e.g. signature verification, still needs to cross check with iOS or web open source codebase
- No move for file and folders, thumbnails, respecting accepted MIME types, max upload size, can't init Proton Drive (coming in V2)
- Assumptions: only one main share per account
## V2
Moving files and folders are [features](https://github.com/rclone/rclone/blob/51a468b2bae4ca8e21760435211623a8199a9167/fs/features.go#L25)
- [ ] Folder
- [ ] (Feature) Update (force overwrite)
- [ ] (Feature) Move
- [ ] Commit back to proton-go-api and switch to using upstream (make sure the tag is at the tip though)
- [ ] Support legacy 2-password mode
- [ ] Support thumbnail
- [ ] Proton Drive init (no prior Proton Drive login before -> probably will have no key, volume, etc. to start with at all)
- [ ] linkID caching -> would need to listen to the event api though
# Questions
- [x] rclone's folder / file rename detection? -> just implement the interface and rclone will deal with the rest!
- [ ] How often will we run into 429 on login

96
common/config.go Normal file
View File

@@ -0,0 +1,96 @@
package common
import "os"
type Config struct {
/* Login */
FirstLoginCredential *FirstLoginCredentialData
ReusableCredential *ReusableCredentialData
UseReusableLogin bool
CredentialCacheFile string // If CredentialCacheFile is empty, no credential will be logged
RefreshAccessToken bool
/* Setting */
DestructiveIntegrationTest bool // CAUTION: the integration test requires a clean proton drive
EmptyTrashAfterIntegrationTest bool // CAUTION: the integration test will clean up all the data in the trash
/* Drive */
DataFolderName string
}
type FirstLoginCredentialData struct {
Username string
Password string
TwoFA string
}
type ReusableCredentialData struct {
UID string
AccessToken string
RefreshToken string
SaltedKeyPass string // []byte <-> base64
}
func NewConfigWithDefaultValues() *Config {
return &Config{
// login
FirstLoginCredential: &FirstLoginCredentialData{
Username: "",
Password: "",
TwoFA: "",
},
ReusableCredential: &ReusableCredentialData{
UID: "",
AccessToken: "",
RefreshToken: "",
SaltedKeyPass: "", // []byte <-> base64
},
UseReusableLogin: false,
CredentialCacheFile: "",
RefreshAccessToken: false,
DestructiveIntegrationTest: false,
EmptyTrashAfterIntegrationTest: false,
DataFolderName: "data",
}
}
func NewConfigForIntegrationTests() *Config {
username := os.Getenv("PROTON_API_BRIDGE_TEST_USERNAME")
password := os.Getenv("PROTON_API_BRIDGE_TEST_PASSWORD")
twoFA := os.Getenv("PROTON_API_BRIDGE_TEST_TWOFA")
useReusableLoginStr := os.Getenv("PROTON_API_BRIDGE_TEST_USE_REUSABLE_LOGIN")
useReusableLogin := false
if useReusableLoginStr == "1" {
useReusableLogin = true
}
uid := os.Getenv("PROTON_API_BRIDGE_TEST_UID")
accessToken := os.Getenv("PROTON_API_BRIDGE_TEST_ACCESS_TOKEN")
refreshToken := os.Getenv("PROTON_API_BRIDGE_TEST_REFRESH_TOKEN")
saltedKeyPass := os.Getenv("PROTON_API_BRIDGE_TEST_SALTEDKEYPASS")
return &Config{
FirstLoginCredential: &FirstLoginCredentialData{
Username: username,
Password: password,
TwoFA: twoFA,
},
ReusableCredential: &ReusableCredentialData{
UID: uid,
AccessToken: accessToken,
RefreshToken: refreshToken,
SaltedKeyPass: saltedKeyPass, // []byte <-> base64
},
UseReusableLogin: useReusableLogin,
CredentialCacheFile: ".credential",
RefreshAccessToken: false,
DestructiveIntegrationTest: true,
EmptyTrashAfterIntegrationTest: true,
DataFolderName: "data",
}
}

11
common/error.go Normal file
View File

@@ -0,0 +1,11 @@
package common
import "errors"
var (
ErrKeyPassOrSaltedKeyPassMustBeNotNil = errors.New("either keyPass or saltedKeyPass must be not nil")
ErrFailedToUnlockUserKeys = errors.New("failed to unlock user keys")
ErrUsernameAndPasswordRequired = errors.New("username and password are required")
Err2FACodeRequired = errors.New("this account requires a 2FA code")
)

70
common/keyring.go Normal file
View File

@@ -0,0 +1,70 @@
package common
import (
"context"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/henrybear327/go-proton-api"
)
/*
The Proton account keys are organized in the following hierarchy.
An account has some users, each of the user will have one or more user keys.
Each of the user will have some addresses, each of the address will have one or more address keys.
A key is encrypted by a passphrase, and the passphrase is encrypted by another key.
The address keyrings are encrypted with the primary user keyring at the time.
The primary address key is used to create (encrypt) and retrieve (decrypt) data, e.g. shares
*/
func getAccountKRs(ctx context.Context, c *proton.Client, keyPass, saltedKeyPass []byte) (*crypto.KeyRing, map[string]*crypto.KeyRing, []proton.Address, []byte, error) {
/* Code taken and modified from proton-bridge */
user, err := c.GetUser(ctx)
if err != nil {
return nil, nil, nil, nil, err
}
// log.Printf("user %#v", user)
addr, err := c.GetAddresses(ctx)
if err != nil {
return nil, nil, nil, nil, err
}
// log.Printf("addr %#v", addr)
if saltedKeyPass == nil {
if keyPass == nil {
return nil, nil, nil, nil, ErrKeyPassOrSaltedKeyPassMustBeNotNil
}
/*
Notes for -> BUG: Access token does not have sufficient scope
Only within the first x minutes that the user logs in with username and password, the getSalts route will be available to be called!
*/
salts, err := c.GetSalts(ctx)
if err != nil {
return nil, nil, nil, nil, err
}
// log.Printf("salts %#v", salts)
saltedKeyPass, err = salts.SaltForKey(keyPass, user.Keys.Primary().ID)
if err != nil {
return nil, nil, nil, nil, err
}
// log.Printf("saltedKeyPass ok")
}
userKR, addrKRs, err := proton.Unlock(user, addr, saltedKeyPass, nil)
if err != nil {
return nil, nil, nil, nil, err
} else if userKR.CountDecryptionEntities() == 0 {
if err != nil {
return nil, nil, nil, nil, ErrFailedToUnlockUserKeys
}
}
return userKR, addrKRs, addr, saltedKeyPass, nil
}

28
common/proton_manager.go Normal file
View File

@@ -0,0 +1,28 @@
package common
import (
"github.com/henrybear327/go-proton-api"
)
// TODO: use proper appname and version
func AppVersion() string {
// return "web-drive@5.0.13.8"
return "ios-drive@1.14.0"
}
func defaultAPIOptions() []proton.Option {
return []proton.Option{
proton.WithAppVersion(AppVersion()),
}
}
func getProtonManager() *proton.Manager {
/*
Notes on API calls
If the app version is not specified, the api calls will be rejected.
*/
m := proton.New(defaultAPIOptions()...)
return m
}

156
common/user.go Normal file
View File

@@ -0,0 +1,156 @@
package common
import (
"context"
"encoding/base64"
"encoding/json"
"log"
"os"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/henrybear327/go-proton-api"
)
func cacheCredentialToFile(config *Config) error {
if config.CredentialCacheFile != "" {
str, err := json.Marshal(config.ReusableCredential)
if err != nil {
return err
}
file, err := os.Create(config.CredentialCacheFile)
if err != nil {
return err
}
defer file.Close()
_, err = file.WriteString(string(str))
if err != nil {
return err
}
}
return nil
}
/*
Log in methods
- username and password to log in
- UID and refresh token
Keyring decryption
The password will be salted, and then used to decrypt the keyring. The salted password needs to be and can be cached, so the keyring can be re-decrypted when needed
*/
func Login(ctx context.Context, config *Config) (*proton.Manager, *proton.Client, *crypto.KeyRing, map[string]*crypto.KeyRing, []proton.Address, error) {
var c *proton.Client
var auth proton.Auth
var userKR *crypto.KeyRing
var addrKRs map[string]*crypto.KeyRing
var addr []proton.Address
// get manager
m := getProtonManager()
if config.UseReusableLogin {
/*
Using NewClientWithRefresh so the credential can last longer,
as each run of the program will trigger a access token refresh
*/
var err error
if config.RefreshAccessToken {
c, auth, err = m.NewClientWithRefresh(ctx, config.ReusableCredential.UID, config.ReusableCredential.RefreshToken)
if err != nil {
return nil, nil, nil, nil, nil, err
}
config.ReusableCredential.UID = auth.UID
config.ReusableCredential.AccessToken = auth.AccessToken
config.ReusableCredential.RefreshToken = auth.RefreshToken
} else {
c = m.NewClient(config.ReusableCredential.UID, config.ReusableCredential.AccessToken, config.ReusableCredential.RefreshToken)
}
err = cacheCredentialToFile(config)
if err != nil {
return nil, nil, nil, nil, nil, err
}
SaltedKeyPassByteArr, err := base64.StdEncoding.DecodeString(config.ReusableCredential.SaltedKeyPass)
if err != nil {
return nil, nil, nil, nil, nil, err
}
userKR, addrKRs, addr, _, err = getAccountKRs(ctx, c, nil, SaltedKeyPassByteArr)
if err != nil {
return nil, nil, nil, nil, nil, err
}
} else {
username := config.FirstLoginCredential.Username
password := config.FirstLoginCredential.Password
if username == "" || password == "" {
return nil, nil, nil, nil, nil, ErrUsernameAndPasswordRequired
}
// perform login
var err error
c, auth, err = m.NewClientWithLogin(ctx, username, []byte(password))
if err != nil {
return nil, nil, nil, nil, nil, err
}
if auth.TwoFA.Enabled&proton.HasTOTP != 0 {
if config.FirstLoginCredential.TwoFA != "" {
err := c.Auth2FA(ctx, proton.Auth2FAReq{
TwoFactorCode: config.FirstLoginCredential.TwoFA,
})
if err != nil {
return nil, nil, nil, nil, nil, err
}
} else {
return nil, nil, nil, nil, nil, Err2FACodeRequired
}
}
// decrypt keyring
var saltedKeyPassByteArr []byte
userKR, addrKRs, addr, saltedKeyPassByteArr, err = getAccountKRs(ctx, c, []byte(password), nil)
if err != nil {
return nil, nil, nil, nil, nil, err
}
saltedKeyPass := base64.StdEncoding.EncodeToString(saltedKeyPassByteArr)
config.ReusableCredential.UID = auth.UID
config.ReusableCredential.AccessToken = auth.AccessToken
config.ReusableCredential.RefreshToken = auth.RefreshToken
config.ReusableCredential.SaltedKeyPass = saltedKeyPass
err = cacheCredentialToFile(config)
if err != nil {
return nil, nil, nil, nil, nil, err
}
}
return m, c, userKR, addrKRs, addr, nil
}
func Logout(ctx context.Context, config *Config, m *proton.Manager, c *proton.Client, userKR *crypto.KeyRing, addrKRs map[string]*crypto.KeyRing) error {
defer m.Close()
defer c.Close()
if config.CredentialCacheFile == "" {
log.Println("Logging out user")
// log out
err := c.AuthDelete(ctx)
if err != nil {
return err
}
// clear keyrings
userKR.ClearPrivateParams()
for i := range addrKRs {
addrKRs[i].ClearPrivateParams()
}
}
return nil
}

135
crypto.go Normal file
View File

@@ -0,0 +1,135 @@
package proton_api_bridge
import (
"encoding/base64"
"io"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/gopenpgp/v2/helper"
)
func generatePassphrase() (string, error) {
token, err := crypto.RandomToken(32)
if err != nil {
return "", err
}
tokenBase64 := base64.StdEncoding.EncodeToString(token)
return tokenBase64, nil
}
func generateCryptoKey() (string, string, error) {
passphrase, err := generatePassphrase()
if err != nil {
return "", "", err
}
// all hardcoded values from iOS drive
key, err := helper.GenerateKey("Drive key", "noreply@protonmail.com", []byte(passphrase), "x25519", 0)
if err != nil {
return "", "", err
}
return passphrase, key, nil
}
// taken from Proton Go API Backend
func encryptWithSignature(kr, addrKR *crypto.KeyRing, b []byte) (string, string, error) {
enc, err := kr.Encrypt(crypto.NewPlainMessage(b), nil)
if err != nil {
return "", "", err
}
encArm, err := enc.GetArmored()
if err != nil {
return "", "", err
}
sig, err := addrKR.SignDetached(crypto.NewPlainMessage(b))
if err != nil {
return "", "", err
}
sigArm, err := sig.GetArmored()
if err != nil {
return "", "", err
}
return encArm, sigArm, nil
}
func generateNodeKeys(kr, addrKR *crypto.KeyRing) (string, string, string, error) {
nodePassphrase, nodeKey, err := generateCryptoKey()
if err != nil {
return "", "", "", err
}
nodePassphraseEnc, nodePassphraseSignature, err := encryptWithSignature(kr, addrKR, []byte(nodePassphrase))
if err != nil {
return "", "", "", err
}
return nodeKey, nodePassphraseEnc, nodePassphraseSignature, nil
}
func getKeyRing(kr, addrKR *crypto.KeyRing, key, passphrase, passphraseSignature string) (*crypto.KeyRing, error) {
enc, err := crypto.NewPGPMessageFromArmored(passphrase)
if err != nil {
return nil, err
}
dec, err := kr.Decrypt(enc, nil, crypto.GetUnixTime())
if err != nil {
return nil, err
}
sig, err := crypto.NewPGPSignatureFromArmored(passphraseSignature)
if err != nil {
return nil, err
}
if err := addrKR.VerifyDetached(dec, sig, crypto.GetUnixTime()); err != nil {
return nil, err
}
lockedKey, err := crypto.NewKeyFromArmored(key)
if err != nil {
return nil, err
}
unlockedKey, err := lockedKey.Unlock(dec.GetBinary())
if err != nil {
return nil, err
}
return crypto.NewKeyRing(unlockedKey)
}
func decryptBlockIntoBuffer(sessionKey *crypto.SessionKey, addrKR, nodeKR *crypto.KeyRing, encSignature string, buffer io.ReaderFrom, block io.ReadCloser) error {
data, err := io.ReadAll(block)
if err != nil {
return err
}
plainMessage, err := sessionKey.Decrypt(data)
if err != nil {
return err
}
encSignatureArm, err := crypto.NewPGPMessageFromArmored(encSignature)
if err != nil {
return err
}
err = addrKR.VerifyDetachedEncrypted(plainMessage, encSignatureArm, nodeKR, crypto.GetUnixTime())
if err != nil {
return err
}
_, err = buffer.ReadFrom(plainMessage.NewReader())
if err != nil {
return err
}
return nil
}

105
delete.go Normal file
View File

@@ -0,0 +1,105 @@
package proton_api_bridge
import (
"context"
"github.com/henrybear327/go-proton-api"
)
func (protonDrive *ProtonDrive) moveToTrash(ctx context.Context, parentLinkID string, linkIDs ...string) error {
/*
Assumption:
- only operates on main share
*/
err := protonDrive.c.TrashChildren(ctx, protonDrive.MainShare.ShareID, parentLinkID, linkIDs...)
if err != nil {
return err
}
return nil
}
func (protonDrive *ProtonDrive) MoveFileToTrashByID(ctx context.Context, linkID string) error {
fileLink, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, linkID)
if err != nil {
return err
}
if fileLink.Type != proton.LinkTypeFile {
return ErrLinkTypeMustToBeFolderType
}
return protonDrive.moveToTrash(ctx, fileLink.ParentLinkID, linkID)
}
func (protonDrive *ProtonDrive) MoveFolderToTrashByID(ctx context.Context, linkID string, onlyOnEmpty bool) error {
folderLink, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, linkID)
if err != nil {
return err
}
if folderLink.Type != proton.LinkTypeFolder {
return ErrLinkTypeMustToBeFolderType
}
childrenLinks, err := protonDrive.c.ListChildren(ctx, protonDrive.MainShare.ShareID, linkID, false)
if err != nil {
return err
}
if onlyOnEmpty {
if len(childrenLinks) > 0 {
return ErrFolderIsNotEmpty
}
}
return protonDrive.moveToTrash(ctx, folderLink.ParentLinkID, linkID)
}
// WARNING!!!!
// Everything in the root folder will be moved to trash
// Most likely only used for debugging when the key is messed up
func (protonDrive *ProtonDrive) EmptyRootFolder(ctx context.Context) error {
links, err := protonDrive.c.ListChildren(ctx, protonDrive.MainShare.ShareID, protonDrive.MainShare.LinkID, true)
if err != nil {
return err
}
{
linkIDs := make([]string, 0)
for i := range links {
if links[i].State == proton.LinkStateActive /* use TrashChildren */ {
linkIDs = append(linkIDs, links[i].LinkID)
}
}
err := protonDrive.c.TrashChildren(ctx, protonDrive.MainShare.ShareID, protonDrive.MainShare.LinkID, linkIDs...)
if err != nil {
return err
}
}
{
linkIDs := make([]string, 0)
for i := range links {
if links[i].State != proton.LinkStateActive {
linkIDs = append(linkIDs, links[i].LinkID)
}
}
err := protonDrive.c.DeleteChildren(ctx, protonDrive.MainShare.ShareID, protonDrive.MainShare.LinkID, linkIDs...)
if err != nil {
return err
}
}
return nil
}
// Empty the trash
func (protonDrive *ProtonDrive) EmptyTrash(ctx context.Context) error {
err := protonDrive.c.EmptyTrash(ctx, protonDrive.MainShare.ShareID)
if err != nil {
return err
}
return nil
}

162
drive.go Normal file
View File

@@ -0,0 +1,162 @@
package proton_api_bridge
import (
"context"
"log"
"github.com/henrybear327/Proton-API-Bridge/common"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/henrybear327/go-proton-api"
)
type ProtonDrive struct {
MainShare *proton.Share
RootLink *proton.Link
MainShareKR *crypto.KeyRing
AddrKR *crypto.KeyRing
Config *common.Config
c *proton.Client
m *proton.Manager
userKR *crypto.KeyRing
addrKRs map[string]*crypto.KeyRing
addrData []proton.Address
signatureAddress string
}
func NewDefaultConfig() *common.Config {
return common.NewConfigWithDefaultValues()
}
func NewProtonDrive(ctx context.Context, config *common.Config) (*ProtonDrive, error) {
/* Log in and logout */
m, c, userKR, addrKRs, addrData, err := common.Login(ctx, config)
if err != nil {
return nil, err
}
/*
Current understanding (at the time of the commit)
The volume is the mount point.
A link is like a folder in POSIX.
A share is associated with a link to represent the access control,
and serves as an entry point to a location in the file structure (Volume).
It points to a link, of file or folder type, anywhere in the tree and holds a key called the ShareKey.
To access a link, of file or folder type, a user must be a member of a share.
A volume has a default share for access control and is owned by the creator of the volume.
A volume has a default link as it's root folder.
MIMETYPE holds type, e.g. folder, image/png, etc.
*/
volumes, err := listAllVolumes(ctx, c)
if err != nil {
return nil, err
}
// log.Printf("all volumes %#v", volumes)
mainShareID := ""
for i := range volumes {
// iOS drive: first active volume
if volumes[i].State == proton.VolumeStateActive {
mainShareID = volumes[i].Share.ShareID
}
}
// log.Println("total volumes", len(volumes), "mainShareID", mainShareID)
/* Get root folder from the main share of the volume */
mainShare, err := getShareByID(ctx, c, mainShareID)
if err != nil {
return nil, err
}
// check for main share integrity
{
mainShareCheck := false
shares, err := getAllShares(ctx, c)
if err != nil {
return nil, err
}
for i := range shares {
if shares[i].ShareID == mainShare.ShareID &&
shares[i].LinkID == mainShare.LinkID &&
shares[i].Flags == proton.PrimaryShare &&
shares[i].Type == proton.ShareTypeMain {
mainShareCheck = true
}
}
if !mainShareCheck {
log.Printf("mainShare %#v", mainShare)
log.Printf("shares %#v", shares)
return nil, ErrMainSharePreconditionsFailed
}
}
// Note: rootLink's parentLinkID == ""
/*
Link holds the tree structure, for the clients, they represent the files and folders of a given volume.
They have a ParentLinkID that points to parent folders.
Links also hold the file name (encrypted) and a hash of the name for name collisions.
Link data is encrypted with its owning Share keyring.
*/
rootLink, err := c.GetLink(ctx, mainShare.ShareID, mainShare.LinkID)
if err != nil {
return nil, err
}
if err != nil {
return nil, err
}
// log.Printf("rootLink %#v", rootLink)
// log.Printf("addrKRs %#v", addrKRs)=
addrKR := addrKRs[mainShare.AddressID]
// log.Println("addrKR CountDecryptionEntities", addrKR.CountDecryptionEntities())
mainShareKR, err := mainShare.GetKeyRing(addrKR)
if err != nil {
return nil, err
}
// log.Println("mainShareKR CountDecryptionEntities", mainShareKR.CountDecryptionEntities())
return &ProtonDrive{
MainShare: mainShare,
RootLink: &rootLink,
MainShareKR: mainShareKR,
AddrKR: addrKR,
Config: config,
c: c,
m: m,
userKR: userKR,
addrKRs: addrKRs,
addrData: addrData,
signatureAddress: mainShare.Creator,
}, nil
}
func (protonDrive *ProtonDrive) Logout(ctx context.Context) error {
return common.Logout(ctx, protonDrive.Config, protonDrive.m, protonDrive.c, protonDrive.userKR, protonDrive.addrKRs)
}
func (protonDrive *ProtonDrive) About(ctx context.Context) (*proton.User, error) {
user, err := protonDrive.c.GetUser(ctx)
if err != nil {
return nil, err
}
return &user, nil
}
func (protonDrive *ProtonDrive) GetLink(ctx context.Context, linkID string) (*proton.Link, error) {
link, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, linkID)
return &link, err
}

547
drive_test.go Normal file
View File

@@ -0,0 +1,547 @@
package proton_api_bridge
import (
"bufio"
"bytes"
"context"
"os"
"testing"
"github.com/henrybear327/Proton-API-Bridge/common"
"github.com/henrybear327/Proton-API-Bridge/utility"
)
/* Helper functions */
func setup(t *testing.T) (context.Context, context.CancelFunc, *ProtonDrive) {
utility.SetupLog()
config := common.NewConfigForIntegrationTests()
{
// pre-condition check
if config.DestructiveIntegrationTest == false {
t.Fatalf("CAUTION: the integration test requires a clean proton drive")
}
if config.EmptyTrashAfterIntegrationTest == false {
t.Fatalf("CAUTION: the integration test requires cleaning up the drive after running the tests")
}
}
ctx, cancel := context.WithCancel(context.Background())
protonDrive, err := NewProtonDrive(ctx, config)
if err != nil {
t.Fatal(err)
}
err = protonDrive.EmptyRootFolder(ctx)
if err != nil {
t.Fatal(err)
}
err = protonDrive.EmptyTrash(ctx)
if err != nil {
t.Fatal(err)
}
return ctx, cancel, protonDrive
}
func tearDown(t *testing.T, ctx context.Context, protonDrive *ProtonDrive) {
if protonDrive.Config.EmptyTrashAfterIntegrationTest {
err := protonDrive.EmptyTrash(ctx)
if err != nil {
t.Fatal(err)
}
}
}
/* Integration Tests */
func TestCreateAndDeleteFolderAtRoot(t *testing.T) {
ctx, cancel, protonDrive := setup(t)
t.Cleanup(func() {
defer cancel()
defer tearDown(t, ctx, protonDrive)
})
{
/* Create folder tmp */
_, err := protonDrive.CreateNewFolderByID(ctx, protonDrive.RootLink.LinkID, "tmp")
if err != nil {
t.Fatal(err)
}
paths := make([]string, 0)
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, true, "", &paths)
if err != nil {
t.Fatal(err)
}
if len(paths) != 1 {
t.Fatalf("Total path returned is differs from expected: %#v", paths)
}
if paths[0] != "/tmp" {
t.Fatalf("Wrong folder created")
}
paths = make([]string, 0)
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, false, "", &paths)
if err != nil {
t.Fatal(err)
}
if len(paths) != 2 {
t.Fatalf("Total path returned is differs from expected: %#v", paths)
}
if paths[0] != "/root" {
t.Fatalf("Wrong root folder")
}
if paths[1] != "/root/tmp" {
t.Fatalf("Wrong folder created")
}
}
{
/* Delete folder tmp */
targetFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, "tmp", true)
if err != nil {
t.Fatal(err)
}
if targetFolderLink == nil {
t.Fatalf("Folder tmp not found")
} else {
err = protonDrive.MoveFolderToTrashByID(ctx, targetFolderLink.LinkID, false)
if err != nil {
t.Fatal(err)
}
}
paths := make([]string, 0)
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, false, "", &paths)
if err != nil {
t.Fatal(err)
}
if len(paths) != 1 {
t.Fatalf("Total path returned is differs from expected: %#v", paths)
}
if paths[0] != "/root" {
t.Fatalf("Wrong root folder")
}
}
}
func TestUploadAndDownloadAndDeleteAFileAtRoot(t *testing.T) {
ctx, cancel, protonDrive := setup(t)
t.Cleanup(func() {
defer cancel()
defer tearDown(t, ctx, protonDrive)
})
{
/* Upload a file integrationTestImage.png */
f, err := os.Open("testcase/integrationTestImage.png")
if err != nil {
t.Fatal(err)
}
defer f.Close()
info, err := os.Stat("testcase/integrationTestImage.png")
if err != nil {
t.Fatal(err)
}
in := bufio.NewReader(f)
_, err = protonDrive.UploadFileByReader(ctx, protonDrive.RootLink.LinkID, "integrationTestImage.png", info.ModTime(), in)
if err != nil {
t.Fatal(err)
}
paths := make([]string, 0)
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, true, "", &paths)
if err != nil {
t.Fatal(err)
}
if len(paths) != 1 {
t.Fatalf("Total path returned is not as expected: %#v", paths)
}
if paths[0] != "/integrationTestImage.png" {
t.Fatalf("Wrong file name decrypted")
}
paths = make([]string, 0)
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, false, "", &paths)
if err != nil {
t.Fatal(err)
}
if len(paths) != 2 {
t.Fatalf("Total path returned is differs from expected: %#v", paths)
}
if paths[0] != "/root" {
t.Fatalf("Wrong root folder")
}
if paths[1] != "/root/integrationTestImage.png" {
t.Fatalf("Wrong file name decrypted")
}
}
{
/* Download a file integrationTestImage.png */
targetFileLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, "integrationTestImage.png", false)
if err != nil {
t.Fatal(err)
}
if targetFileLink == nil {
t.Fatalf("File integrationTestImage.png not found")
} else {
{
_, err := protonDrive.SearchByNameInFolder(ctx, targetFileLink, "integrationTestImage.png", true, false)
if err != ErrLinkTypeMustToBeFolderType {
t.Fatalf("Wrong error message being returned")
}
}
downloadedData, err := protonDrive.DownloadFileByID(ctx, targetFileLink.LinkID)
if err != nil {
t.Fatal(err)
}
originalData, err := os.ReadFile("testcase/integrationTestImage.png")
if err != nil {
t.Fatal(err)
}
if bytes.Equal(downloadedData, originalData) == false {
t.Fatalf("Downloaded content is different from the original content")
}
}
}
{
/* TODO: Check file metadata */
}
{
/* Delete a file integrationTestImage.png */
targetFileLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, "integrationTestImage.png", false)
if err != nil {
t.Fatal(err)
}
if targetFileLink == nil {
t.Fatalf("File integrationTestImage.png not found")
} else {
err = protonDrive.MoveFileToTrashByID(ctx, targetFileLink.LinkID)
if err != nil {
t.Fatal(err)
}
}
paths := make([]string, 0)
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, false, "", &paths)
if err != nil {
t.Fatal(err)
}
if len(paths) != 1 {
t.Fatalf("Total path returned is differs from expected: %#v", paths)
}
if paths[0] != "/root" {
t.Fatalf("Wrong root folder")
}
}
}
func TestUploadAndDeleteAnEmptyFileAtRoot(t *testing.T) {
ctx, cancel, protonDrive := setup(t)
t.Cleanup(func() {
defer cancel()
defer tearDown(t, ctx, protonDrive)
})
{
/* Upload a file integrationTestImage.png */
_, err := protonDrive.UploadFileByPath(ctx, protonDrive.RootLink, "empty.txt", "testcase/empty.txt")
if err != nil {
t.Fatal(err)
}
paths := make([]string, 0)
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, true, "", &paths)
if err != nil {
t.Fatal(err)
}
if len(paths) != 1 {
t.Fatalf("Total path returned is differs from expected: %#v", paths)
}
if paths[0] != "/empty.txt" {
t.Fatalf("Wrong file name decrypted")
}
paths = make([]string, 0)
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, false, "", &paths)
if err != nil {
t.Fatal(err)
}
if len(paths) != 2 {
t.Fatalf("Total path returned is differs from expected: %#v", paths)
}
if paths[0] != "/root" {
t.Fatalf("Wrong root folder")
}
if paths[1] != "/root/empty.txt" {
t.Fatalf("Wrong file name decrypted")
}
}
{
/* Download a file empty.txt */
targetFileLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, "empty.txt", false)
if err != nil {
t.Fatal(err)
}
if targetFileLink == nil {
t.Fatalf("File empty.txt not found")
} else {
downloadedData, err := protonDrive.DownloadFileByID(ctx, targetFileLink.LinkID)
if err != nil {
t.Fatal(err)
}
originalData, err := os.ReadFile("testcase/empty.txt")
if err != nil {
t.Fatal(err)
}
if bytes.Equal(downloadedData, originalData) == false {
t.Fatalf("Downloaded content is different from the original content")
}
}
}
{
/* TODO: Check file metadata */
}
{
/* Delete a file empty.txt */
targetFileLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, "empty.txt", false)
if err != nil {
t.Fatal(err)
}
if targetFileLink == nil {
t.Fatalf("File empty.txt not found")
} else {
err = protonDrive.MoveFileToTrashByID(ctx, targetFileLink.LinkID)
if err != nil {
t.Fatal(err)
}
}
paths := make([]string, 0)
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, false, "", &paths)
if err != nil {
t.Fatal(err)
}
if len(paths) != 1 {
t.Fatalf("Total path returned is differs from expected: %#v", paths)
}
if paths[0] != "/root" {
t.Fatalf("Wrong root folder")
}
}
}
func TestUploadAndDownloadAndDeleteAFileAtAFolderOneLevelFromRoot(t *testing.T) {
ctx, cancel, protonDrive := setup(t)
t.Cleanup(func() {
defer cancel()
defer tearDown(t, ctx, protonDrive)
})
{
/* Upload a file integrationTestImage.png */
_, err := protonDrive.CreateNewFolder(ctx, protonDrive.RootLink, "tmp")
if err != nil {
t.Fatal(err)
}
targetFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, "tmp", true)
if err != nil {
t.Fatal(err)
}
if targetFolderLink == nil {
t.Fatalf("Folder tmp not found")
}
_, err = protonDrive.UploadFileByPath(ctx, targetFolderLink, "integrationTestImage.png", "testcase/integrationTestImage.png")
if err != nil {
t.Fatal(err)
}
paths := make([]string, 0)
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, true, "", &paths)
if err != nil {
t.Fatal(err)
}
if len(paths) != 2 {
t.Fatalf("Total path returned is differs from expected: %#v", paths)
}
if paths[0] != "/tmp" {
t.Fatalf("Wrong folder name decrypted")
}
if paths[1] != "/tmp/integrationTestImage.png" {
t.Fatalf("Wrong file name decrypted")
}
paths = make([]string, 0)
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, false, "", &paths)
if err != nil {
t.Fatal(err)
}
if len(paths) != 3 {
t.Fatalf("Total path returned is differs from expected: %#v", paths)
}
if paths[0] != "/root" {
t.Fatalf("Wrong root folder")
}
if paths[1] != "/root/tmp" {
t.Fatalf("Wrong folder name decrypted")
}
if paths[2] != "/root/tmp/integrationTestImage.png" {
t.Fatalf("Wrong file name decrypted")
}
}
{
/* Download a file integrationTestImage.png */
targetFileLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, "integrationTestImage.png", false)
if err != nil {
t.Fatal(err)
}
{
targetFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, "tmp", true)
if err != nil {
t.Fatal(err)
}
if targetFolderLink == nil {
t.Fatalf("Folder tmp not found")
} else {
fileLink, err := protonDrive.SearchByNameInFolder(ctx, targetFolderLink, "integrationTestImage.png", true, false)
if err != nil {
t.Fatal(err)
}
if fileLink.LinkID != targetFileLink.LinkID {
t.Fatalf("Wrong file being returned")
}
}
targetFileLink2, err := protonDrive.SearchByNameRecursively(ctx, targetFolderLink, "integrationTestImage.png", false)
if err != nil {
t.Fatal(err)
}
if targetFileLink.LinkID != targetFileLink2.LinkID {
t.Fatalf("SearchByNameRecursively is broken")
}
}
if targetFileLink == nil {
t.Fatalf("File integrationTestImage.png not found")
} else {
downloadedData, err := protonDrive.DownloadFile(ctx, targetFileLink)
if err != nil {
t.Fatal(err)
}
originalData, err := os.ReadFile("testcase/integrationTestImage.png")
if err != nil {
t.Fatal(err)
}
if bytes.Equal(downloadedData, originalData) == false {
t.Fatalf("Downloaded content is different from the original content")
}
}
}
{
/* TODO: Check file metadata */
}
{
/* Delete a file integrationTestImage.png */
targetFileLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, "integrationTestImage.png", false)
if err != nil {
t.Fatal(err)
}
if targetFileLink == nil {
t.Fatalf("File integrationTestImage.png not found")
} else {
err = protonDrive.MoveFileToTrashByID(ctx, targetFileLink.LinkID)
if err != nil {
t.Fatal(err)
}
}
paths := make([]string, 0)
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, false, "", &paths)
if err != nil {
t.Fatal(err)
}
if len(paths) != 2 {
t.Fatalf("Total path returned is differs from expected: %#v", paths)
}
if paths[0] != "/root" {
t.Fatalf("Wrong root folder")
}
if paths[1] != "/root/tmp" {
t.Fatalf("Wrong tmp folder")
}
}
{
/* Delete a folder tmp */
targetFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, "tmp", true)
if err != nil {
t.Fatal(err)
}
if targetFolderLink == nil {
t.Fatalf("Folder tmp not found")
} else {
err = protonDrive.MoveFolderToTrashByID(ctx, targetFolderLink.LinkID, false)
if err != nil {
t.Fatal(err)
}
}
paths := make([]string, 0)
err = protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, false, "", &paths)
if err != nil {
t.Fatal(err)
}
if len(paths) != 1 {
t.Fatalf("Total path returned is differs from expected: %#v", paths)
}
if paths[0] != "/root" {
t.Fatalf("Wrong root folder")
}
}
}
/*
TODO
- Revision
- Rename
- Move
*/

11
error.go Normal file
View File

@@ -0,0 +1,11 @@
package proton_api_bridge
import "errors"
var (
ErrMainSharePreconditionsFailed = errors.New("the main share assumption has failed")
ErrDataFolderNameIsEmpty = errors.New("please supply a DataFolderName to enabling file downloading")
ErrLinkTypeMustToBeFolderType = errors.New("the link type must be of folder type")
ErrLinkTypeMustToBeFileType = errors.New("the link type must be of file type")
ErrFolderIsNotEmpty = errors.New("folder can't be deleted becuase it is not empty")
)

404
file.go Normal file
View File

@@ -0,0 +1,404 @@
package proton_api_bridge
import (
"bufio"
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"io"
"os"
"time"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/gabriel-vasile/mimetype"
"github.com/henrybear327/go-proton-api"
)
func (protonDrive *ProtonDrive) DownloadFileByID(ctx context.Context, linkID string) ([]byte, error) {
link, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, linkID)
if err != nil {
return nil, err
}
return protonDrive.DownloadFile(ctx, &link)
}
func (protonDrive *ProtonDrive) DownloadFile(ctx context.Context, link *proton.Link) ([]byte, error) {
if link.Type != proton.LinkTypeFile {
return nil, ErrLinkTypeMustToBeFileType
}
parentNodeKR, err := protonDrive.getNodeKRByID(ctx, link.ParentLinkID)
if err != nil {
return nil, err
}
nodeKR, err := link.GetKeyRing(parentNodeKR, protonDrive.AddrKR)
if err != nil {
return nil, err
}
sessionKey, err := link.GetSessionKey(protonDrive.AddrKR, nodeKR)
if err != nil {
return nil, err
}
revisions, err := protonDrive.c.ListRevisions(ctx, protonDrive.MainShare.ShareID, link.LinkID)
if err != nil {
return nil, err
}
// log.Printf("revisions %#v", revisions)
// Revisions are only for files, they represent “versions” of files.
// Each file can have 1 active revision and n obsolete revisions.
activeRevision := -1
for i := range revisions {
if revisions[i].State == proton.RevisionStateActive {
activeRevision = i
}
}
// FIXME: compute total blocks required
// TODO: handle large file downloading
revision, err := protonDrive.c.GetRevision(ctx, protonDrive.MainShare.ShareID, link.LinkID, revisions[activeRevision].ID, 1, 50)
if err != nil {
return nil, err
}
buffer := bytes.NewBuffer(nil)
for i := range revision.Blocks {
// parallel download
blockReader, err := protonDrive.c.GetBlock(ctx, revision.Blocks[i].BareURL, revision.Blocks[i].Token)
if err != nil {
return nil, err
}
defer blockReader.Close()
err = decryptBlockIntoBuffer(sessionKey, protonDrive.AddrKR, nodeKR, revision.Blocks[i].EncSignature, buffer, blockReader)
if err != nil {
return nil, err
}
}
return buffer.Bytes(), nil
}
func (protonDrive *ProtonDrive) UploadFileByReader(ctx context.Context, parentLinkID string, filename string, modTime time.Time, file io.Reader) (*proton.Link, error) {
parentLink, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, parentLinkID)
if err != nil {
return nil, err
}
return protonDrive.uploadFile(ctx, &parentLink, filename, time.Now() /* FIXME */, file)
}
func (protonDrive *ProtonDrive) UploadFileByPath(ctx context.Context, parentLink *proton.Link, filename string, filePath string) (*proton.Link, error) {
f, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer f.Close()
info, err := os.Stat(filePath)
if err != nil {
return nil, err
}
in := bufio.NewReader(f)
return protonDrive.uploadFile(ctx, parentLink, filename, info.ModTime(), in)
}
func (protonDrive *ProtonDrive) uploadFile(ctx context.Context, parentLink *proton.Link, filename string, modTime time.Time, file io.Reader) (*proton.Link, error) {
// FIXME: check iOS: optimize for large files -> enc blocks on the fly
/*
Assumptions:
- Upload is always done to the mainShare
*/
// TODO: check for duplicated filename by using checkAvailableHashes
parentNodeKR, err := protonDrive.getNodeKR(ctx, parentLink)
if err != nil {
return nil, err
}
// detect MIME type
fileContent, err := io.ReadAll(file)
if err != nil {
return nil, err
}
mimetype.SetLimit(0)
mType := mimetype.Detect(fileContent)
mimeType := mType.String()
// log.Println("Detected MIME type", mimeType)
/* step 1: create a draft */
newNodeKey, newNodePassphraseEnc, newNodePassphraseSignature, err := generateNodeKeys(parentNodeKR, protonDrive.AddrKR)
if err != nil {
return nil, err
}
createFileReq := proton.CreateFileReq{
ParentLinkID: parentLink.LinkID,
// Name string // Encrypted File Name
// Hash string // Encrypted File Name hash
MIMEType: mimeType, // MIME Type
// ContentKeyPacket string // The block's key packet, encrypted with the node key.
// ContentKeyPacketSignature string // Unencrypted signature of the content session key, signed with the NodeKey
NodeKey: newNodeKey, // The private NodeKey, used to decrypt any file/folder content.
NodePassphrase: newNodePassphraseEnc, // The passphrase used to unlock the NodeKey, encrypted by the owning Link/Share keyring.
NodePassphraseSignature: newNodePassphraseSignature, // The signature of the NodePassphrase
ModifyTime: modTime.Unix(), // The modified time
SignatureAddress: protonDrive.signatureAddress, // Signature email address used to sign passphrase and name
}
/* Name is encrypted using the parent's keyring, and signed with address key */
err = createFileReq.SetName(filename, protonDrive.AddrKR, parentNodeKR)
if err != nil {
return nil, err
}
parentHashKey, err := parentLink.GetHashKey(parentNodeKR)
if err != nil {
return nil, err
}
newNodeKR, err := getKeyRing(parentNodeKR, protonDrive.AddrKR, newNodeKey, newNodePassphraseEnc, newNodePassphraseSignature)
if err != nil {
return nil, err
}
err = createFileReq.SetHash(filename, parentHashKey)
if err != nil {
return nil, err
}
err = createFileReq.SetContentKeyPacketAndSignature(newNodeKR, protonDrive.AddrKR)
if err != nil {
return nil, err
}
createFileResp, err := protonDrive.c.CreateFile(ctx, protonDrive.MainShare.ShareID, createFileReq)
if err != nil {
return nil, err
}
if len(fileContent) == 0 {
/* step 2 [Skipped]: upload blocks and collect block data */
/* step 3: mark the file as active by updating the revision */
manifestSignatureData := make([]byte, 0)
manifestSignature, err := protonDrive.AddrKR.SignDetached(crypto.NewPlainMessage(manifestSignatureData))
if err != nil {
return nil, err
}
manifestSignatureString, err := manifestSignature.GetArmored()
if err != nil {
return nil, err
}
err = protonDrive.c.UpdateRevision(ctx, protonDrive.MainShare.ShareID, createFileResp.ID, createFileResp.RevisionID, proton.UpdateRevisionReq{
BlockList: make([]proton.BlockToken, 0),
State: proton.RevisionStateActive,
ManifestSignature: manifestSignatureString,
SignatureAddress: protonDrive.signatureAddress,
})
if err != nil {
return nil, err
}
} else {
/* step 2: upload blocks and collect block data */
// FIXME: handle partial upload (failed midway)
// FIXME: get block size
blockSize := 4 * 1024 * 1024
type PendingUploadBlocks struct {
blockUploadInfo proton.BlockUploadInfo
encData []byte
}
blocks := make([]PendingUploadBlocks, 0)
manifestSignatureData := make([]byte, 0)
sessionKey, err := func() (*crypto.SessionKey, error) {
keyPacket := createFileReq.ContentKeyPacket
keyPacketByteArr, err := base64.StdEncoding.DecodeString(keyPacket)
if err != nil {
return nil, err
}
sessionKey, err := newNodeKR.DecryptSessionKey(keyPacketByteArr)
if err != nil {
return nil, err
}
// FIXME: verify the signature of the session key
// signatureString, err := crypto.NewPGPMessageFromArmored(createFileReq.ContentKeyPacketSignature)
// if err != nil {
// return nil, err
// }
// err = protonDrive.AddrKR.VerifyDetachedEncrypted(crypto.NewPlainMessageFromString(sessionKey.GetBase64Key()), signatureString, newNodeKR, crypto.GetUnixTime())
// if err != nil {
// return nil, err
// }
return sessionKey, nil
}()
if err != nil {
return nil, err
}
for i := 0; i*blockSize < len(fileContent); i++ {
// encrypt data
upperBound := (i + 1) * blockSize
if upperBound > len(fileContent) {
upperBound = len(fileContent)
}
data := fileContent[i*blockSize : upperBound]
dataPlainMessage := crypto.NewPlainMessage(data)
encData, err := sessionKey.Encrypt(dataPlainMessage)
if err != nil {
return nil, err
}
encSignature, err := protonDrive.AddrKR.SignDetachedEncrypted(dataPlainMessage, newNodeKR)
if err != nil {
return nil, err
}
encSignatureStr, err := encSignature.GetArmored()
if err != nil {
return nil, err
}
h := sha256.New()
h.Write(encData)
hash := h.Sum(nil)
base64Hash := base64.StdEncoding.EncodeToString(hash)
if err != nil {
return nil, err
}
manifestSignatureData = append(manifestSignatureData, hash...)
blocks = append(blocks, PendingUploadBlocks{
blockUploadInfo: proton.BlockUploadInfo{
Index: i + 1, // iOS drive: BE starts with 1
Size: int64(len(encData)),
EncSignature: encSignatureStr,
Hash: base64Hash,
},
encData: encData,
})
}
blockList := make([]proton.BlockUploadInfo, 0)
for i := 0; i < len(blocks); i++ {
blockList = append(blockList, blocks[i].blockUploadInfo)
}
blockTokens := make([]proton.BlockToken, 0)
blockUploadReq := proton.BlockUploadReq{
AddressID: protonDrive.MainShare.AddressID,
ShareID: protonDrive.MainShare.ShareID,
LinkID: createFileResp.ID,
RevisionID: createFileResp.RevisionID,
BlockList: blockList,
}
blockUploadResp, err := protonDrive.c.RequestBlockUpload(ctx, blockUploadReq)
if err != nil {
return nil, err
}
for i := range blockUploadResp {
err := protonDrive.c.UploadBlock(ctx, blockUploadResp[i].BareURL, blockUploadResp[i].Token, bytes.NewReader(blocks[i].encData))
if err != nil {
return nil, err
}
blockTokens = append(blockTokens, proton.BlockToken{
Index: i + 1,
Token: blockUploadResp[i].Token,
})
}
/* step 3: mark the file as active by updating the revision */
// TODO: check iOS Drive CommitableRevision
manifestSignature, err := protonDrive.AddrKR.SignDetached(crypto.NewPlainMessage(manifestSignatureData))
if err != nil {
return nil, err
}
manifestSignatureString, err := manifestSignature.GetArmored()
if err != nil {
return nil, err
}
err = protonDrive.c.UpdateRevision(ctx, protonDrive.MainShare.ShareID, createFileResp.ID, createFileResp.RevisionID, proton.UpdateRevisionReq{
BlockList: blockTokens,
State: proton.RevisionStateActive,
ManifestSignature: manifestSignatureString,
SignatureAddress: protonDrive.signatureAddress,
})
if err != nil {
return nil, err
}
}
link, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, createFileResp.ID)
if err != nil {
return nil, err
}
return &link, nil
}
/*
There is a route that proton-go-api doesn't have - checkAvailableHashes.
This is used to quickly find the next available filename when the originally supplied filename is taken in the current folder.
Based on the code below, which is taken from the Proton iOS Drive app, we can infer that:
- when a file is to be uploaded && there is filename conflict after the first upload:
- on web, user will be prompted with a) overwrite b) keep both by appending filename with iteration number c) do nothing
- on the iOS client logic, we can see that when the filename conflict happens (after the upload attampt failed)
- the filename will be hashed by using filename + iteration
- 10 iterations will be done per batch, each iteration's hash will be sent to the server
- the server will return available hashes, and the client will take the lowest iteration as the filename to be used
- will be used to search for the next available filename (using hashes avoids the filename being known to the server)
private func findNextAvailableName(for file: FileNameCheckerModel, offset: Int, completion: @escaping (Result<NameHashPair, Error>) -> Void) {
assert(offset >= 0)
let fileName = file.originalName.fileName()
let `extension` = file.originalName.fileExtension()
var possibleNamesHashPairs = [NameHashPair]()
let lowerBound = offset + 1
let upperBound = offset + step
for iteration in lowerBound...upperBound {
let newName = "\(fileName) (\(iteration))" + (`extension`.isEmpty ? "" : "." + `extension`)
guard let newHash = try? hasher(newName, file.parentNodeHashKey) else { continue }
possibleNamesHashPairs.append(NameHashPair(name: newName, hash: newHash))
}
hashChecker.checkAvailableHashes(among: possibleNamesHashPairs, onFolder: file.parent) { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let approvedHashes) where approvedHashes.isEmpty:
self.findNextAvailableName(for: file, offset: upperBound, completion: completion)
case .success(let approvedHashes):
let approvedPair = possibleNamesHashPairs.first { approvedHashes.contains($0.hash) }!
completion(.success(approvedPair))
}
}
}
*/

236
folder.go Normal file
View File

@@ -0,0 +1,236 @@
package proton_api_bridge
import (
"context"
"log"
"os"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/henrybear327/go-proton-api"
)
type ProtonDirectoryData struct {
Link *proton.Link
Name string
IsFolder bool
}
func (protonDrive *ProtonDrive) ListDirectory(
ctx context.Context,
folderLinkID string) ([]*ProtonDirectoryData, error) {
ret := make([]*ProtonDirectoryData, 0)
folderLink, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, folderLinkID)
if err != nil {
return nil, err
}
if folderLink.State == proton.LinkStateActive {
childrenLinks, err := protonDrive.c.ListChildren(ctx, protonDrive.MainShare.ShareID, folderLink.LinkID, true)
if err != nil {
return nil, err
}
if childrenLinks != nil {
folderParentKR, err := protonDrive.getNodeKRByID(ctx, folderLink.ParentLinkID)
if err != nil {
return nil, err
}
defer folderParentKR.ClearPrivateParams()
folderLinkKR, err := folderLink.GetKeyRing(folderParentKR, protonDrive.AddrKR)
if err != nil {
return nil, err
}
defer folderLinkKR.ClearPrivateParams()
for i := range childrenLinks {
if childrenLinks[i].State != proton.LinkStateActive {
continue
}
name, err := childrenLinks[i].GetName(folderLinkKR, protonDrive.AddrKR)
if err != nil {
return nil, err
}
ret = append(ret, &ProtonDirectoryData{
Link: &childrenLinks[i],
Name: name,
IsFolder: childrenLinks[i].Type == proton.LinkTypeFolder,
})
}
}
}
return ret, nil
}
func (protonDrive *ProtonDrive) ListDirectoriesRecursively(
ctx context.Context,
parentNodeKR *crypto.KeyRing,
link *proton.Link,
download bool,
maxDepth, curDepth /* 0-based */ int,
excludeRoot bool,
pathSoFar string,
paths *[]string) error {
/*
Assumptions:
- we only care about the active ones
- we only operate on the mainShare
*/
if link.State != proton.LinkStateActive {
return nil
}
// log.Println("curDepth", curDepth, "pathSoFar", pathSoFar)
var currentPath = ""
if !(excludeRoot && curDepth == 0) {
name, err := link.GetName(parentNodeKR, protonDrive.AddrKR)
if err != nil {
return err
}
currentPath = pathSoFar + "/" + name
// log.Println("currentPath", currentPath)
if paths != nil {
*paths = append(*paths, currentPath)
}
}
if download {
if protonDrive.Config.DataFolderName == "" {
return ErrDataFolderNameIsEmpty
}
if link.Type == proton.LinkTypeFile {
log.Println("Downloading", currentPath)
defer log.Println("Completes downloading", currentPath)
byteArray, err := protonDrive.DownloadFile(ctx, link)
if err != nil {
return err
}
err = os.WriteFile("./"+protonDrive.Config.DataFolderName+"/"+currentPath, byteArray, 0777)
if err != nil {
return err
}
} else /* folder */ {
if !(excludeRoot && curDepth == 0) {
// log.Println("Creating folder", currentPath)
// defer log.Println("Completes creating folder", currentPath)
err := os.Mkdir("./"+protonDrive.Config.DataFolderName+"/"+currentPath, 0777)
if err != nil {
return err
}
}
}
}
if maxDepth == -1 || curDepth < maxDepth {
if link.Type == proton.LinkTypeFolder {
childrenLinks, err := protonDrive.c.ListChildren(ctx, protonDrive.MainShare.ShareID, link.LinkID, true)
if err != nil {
return err
}
// log.Printf("childrenLinks len = %v, %#v", len(childrenLinks), childrenLinks)
if childrenLinks != nil {
// get current node's keyring
linkKR, err := link.GetKeyRing(parentNodeKR, protonDrive.AddrKR)
if err != nil {
return err
}
defer linkKR.ClearPrivateParams()
for _, childLink := range childrenLinks {
err = protonDrive.ListDirectoriesRecursively(ctx, linkKR, &childLink, download, maxDepth, curDepth+1, excludeRoot, currentPath, paths)
if err != nil {
return err
}
}
}
}
}
return nil
}
func (protonDrive *ProtonDrive) CreateNewFolderByID(ctx context.Context, parentLinkID string, folderName string) (string, error) {
parentLink, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, parentLinkID)
if err != nil {
return "", err
}
return protonDrive.CreateNewFolder(ctx, &parentLink, folderName)
}
func (protonDrive *ProtonDrive) CreateNewFolder(ctx context.Context, parentLink *proton.Link, folderName string) (string, error) {
/*
Assumptions:
- we only operate on the mainShare
*/
// TODO: check for duplicated folder name
parentNodeKR, err := protonDrive.getNodeKR(ctx, parentLink)
if err != nil {
return "", err
}
newNodeKey, newNodePassphraseEnc, newNodePassphraseSignature, err := generateNodeKeys(parentNodeKR, protonDrive.AddrKR)
if err != nil {
return "", err
}
createFolderReq := proton.CreateFolderReq{
ParentLinkID: parentLink.LinkID,
// Name string
// Hash string
NodeKey: newNodeKey,
// NodeHashKey string
NodePassphrase: newNodePassphraseEnc,
NodePassphraseSignature: newNodePassphraseSignature,
SignatureAddress: protonDrive.signatureAddress,
}
/* Name is encrypted using the parent's keyring, and signed with address key */
err = createFolderReq.SetName(folderName, protonDrive.AddrKR, parentNodeKR)
if err != nil {
return "", err
}
parentHashKey, err := parentLink.GetHashKey(parentNodeKR)
if err != nil {
return "", err
}
newNodeKR, err := getKeyRing(parentNodeKR, protonDrive.AddrKR, newNodeKey, newNodePassphraseEnc, newNodePassphraseSignature)
if err != nil {
return "", err
}
err = createFolderReq.SetHash(folderName, parentHashKey)
if err != nil {
return "", err
}
err = createFolderReq.SetNodeHashKey(newNodeKR)
if err != nil {
return "", err
}
createFolderResp, err := protonDrive.c.CreateFolder(ctx, protonDrive.MainShare.ShareID, createFolderReq)
if err != nil {
return "", err
}
// log.Printf("createFolderResp %#v", createFolderResp)
return createFolderResp.ID, nil
}

35
go.mod Normal file
View File

@@ -0,0 +1,35 @@
module github.com/henrybear327/Proton-API-Bridge
go 1.18
require (
github.com/ProtonMail/gopenpgp/v2 v2.7.1
github.com/gabriel-vasile/mimetype v1.4.2
github.com/henrybear327/go-proton-api v0.0.0-20230623063450-66171214ea8c
)
require (
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf // indirect
github.com/ProtonMail/gluon v0.16.1-0.20230526091020-fb7689b15ae3 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230619160724-3fbb1f12458c // indirect
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
github.com/ProtonMail/go-srp v0.0.7 // indirect
github.com/PuerkitoBio/goquery v1.8.1 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/bradenaw/juniper v0.13.0 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/cronokirby/saferith v0.33.0 // indirect
github.com/emersion/go-message v0.16.0 // indirect
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 // indirect
github.com/go-resty/resty/v2 v2.7.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
golang.org/x/crypto v0.10.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/net v0.11.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
)

142
go.sum Normal file
View File

@@ -0,0 +1,142 @@
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
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.16.1-0.20230526091020-fb7689b15ae3 h1:HsRC3WKWY2xf3OGfXnVn1S/EhJx/8dKrWX4/JJQIBc8=
github.com/ProtonMail/gluon v0.16.1-0.20230526091020-fb7689b15ae3/go.mod h1:xYLE11dCH40RrNjkuncXZbYjGyuHKeFtdYKT2nkq6M8=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
github.com/ProtonMail/go-crypto v0.0.0-20230619160724-3fbb1f12458c h1:figwFwYep1Qnl64Y+Rc8tyQWE0xvYAN+5EX+rD40pTU=
github.com/ProtonMail/go-crypto v0.0.0-20230619160724-3fbb1f12458c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
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.7.1 h1:Awsg7MPc2gD3I7IFac2qE3Gdls0lZW8SzrFZ3k1oz0s=
github.com/ProtonMail/gopenpgp/v2 v2.7.1/go.mod h1:/BU5gfAVwqyd8EfC3Eu7zmuhwYQpKs+cGD8M//iiaxs=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/bradenaw/juniper v0.13.0 h1:KKMAiWDkRt45YUNzzw00Jec4nOgWDLVtztjf39E0ppI=
github.com/bradenaw/juniper v0.13.0/go.mod h1:Z2B7aJlQ7xbfWsnMLROj5t/5FQ94/MkIdKC30J4WvzI=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cronokirby/saferith v0.33.0 h1:TgoQlfsD4LIwx71+ChfRcIpjkw+RPOapDEVxa+LhwLo=
github.com/cronokirby/saferith v0.33.0/go.mod h1:QKJhjoqUtBsXCAVEjw38mFqoi7DebT7kthcD7UzbnoA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emersion/go-message v0.16.0 h1:uZLz8ClLv3V5fSFF/fFdW9jXjrZkXIpE1Fn8fKx7pO4=
github.com/emersion/go-message v0.16.0/go.mod h1:pDJDgf/xeUIF+eicT6B/hPX/ZbEorKkUMPOxrPVG2eQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3 h1:hQ1wTMaKcGfobYRT88RM8NFNyX+IQHvagkm/tqViU98=
github.com/emersion/go-vcard v0.0.0-20230331202150-f3d26859ccd3/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/henrybear327/go-proton-api v0.0.0-20230623063450-66171214ea8c h1:9+c5SK3a9k98VpaZid0qDVnGGZ89YsGEz8KMJrwB0FE=
github.com/henrybear327/go-proton-api v0.0.0-20230623063450-66171214ea8c/go.mod h1:l42xBSOrCmkAxzWUHcoUsG/cP8m1hMhV72GoChOX3bg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
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.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
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.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
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.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

51
keyring.go Normal file
View File

@@ -0,0 +1,51 @@
package proton_api_bridge
import (
"context"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/henrybear327/go-proton-api"
)
func (protonDrive *ProtonDrive) getNodeKRByID(ctx context.Context, linkID string) (*crypto.KeyRing, error) {
if linkID == "" {
// most likely someone requested parent link, which happen to be ""
return protonDrive.MainShareKR, nil
}
link, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, linkID)
if err != nil {
return nil, err
}
return protonDrive.getNodeKR(ctx, &link)
}
func (protonDrive *ProtonDrive) getNodeKR(ctx context.Context, link *proton.Link) (*crypto.KeyRing, error) {
if link.ParentLinkID == "" {
nodeKR, err := link.GetKeyRing(protonDrive.MainShareKR, protonDrive.AddrKR)
if err != nil {
return nil, err
}
return nodeKR, nil
}
parentLink, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, link.ParentLinkID)
if err != nil {
return nil, err
}
// parentNodeKR is used to decrypt the current node's KR, as each node has its keyring, which can be decrypted by its parent
parentNodeKR, err := protonDrive.getNodeKR(ctx, &parentLink)
if err != nil {
return nil, err
}
nodeKR, err := link.GetKeyRing(parentNodeKR, protonDrive.AddrKR)
if err != nil {
return nil, err
}
return nodeKR, nil
}

181
search.go Normal file
View File

@@ -0,0 +1,181 @@
package proton_api_bridge
import (
"context"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/henrybear327/go-proton-api"
)
/*
Observation: file name is unique, since it's checked (by hash) on the server
*/
func (protonDrive *ProtonDrive) SearchByNameRecursivelyFromRoot(ctx context.Context, targetName string, isFolder bool) (*proton.Link, error) {
var linkType proton.LinkType
if isFolder {
linkType = proton.LinkTypeFolder
} else {
linkType = proton.LinkTypeFile
}
return protonDrive.searchByNameRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, targetName, linkType)
}
func (protonDrive *ProtonDrive) SearchByNameRecursivelyByID(ctx context.Context, folderLinkID string, targetName string, isFolder bool) (*proton.Link, error) {
folderLink, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, folderLinkID)
if err != nil {
return nil, err
}
var linkType proton.LinkType
if isFolder {
linkType = proton.LinkTypeFolder
} else {
linkType = proton.LinkTypeFile
}
if folderLink.Type != proton.LinkTypeFolder {
return nil, ErrLinkTypeMustToBeFolderType
}
folderKeyRing, err := protonDrive.getNodeKRByID(ctx, folderLink.ParentLinkID)
if err != nil {
return nil, err
}
return protonDrive.searchByNameRecursively(ctx, folderKeyRing, &folderLink, targetName, linkType)
}
func (protonDrive *ProtonDrive) SearchByNameRecursively(ctx context.Context, folderLink *proton.Link, targetName string, isFolder bool) (*proton.Link, error) {
var linkType proton.LinkType
if isFolder {
linkType = proton.LinkTypeFolder
} else {
linkType = proton.LinkTypeFile
}
if folderLink.Type != proton.LinkTypeFolder {
return nil, ErrLinkTypeMustToBeFolderType
}
folderKeyRing, err := protonDrive.getNodeKRByID(ctx, folderLink.ParentLinkID)
if err != nil {
return nil, err
}
return protonDrive.searchByNameRecursively(ctx, folderKeyRing, folderLink, targetName, linkType)
}
func (protonDrive *ProtonDrive) searchByNameRecursively(
ctx context.Context,
parentNodeKR *crypto.KeyRing,
link *proton.Link,
targetName string,
linkType proton.LinkType) (*proton.Link, error) {
/*
Assumptions:
- we only care about the active ones
- we only operate on the mainShare
*/
if link.State != proton.LinkStateActive {
return nil, nil
}
name, err := link.GetName(parentNodeKR, protonDrive.AddrKR)
if err != nil {
return nil, err
}
if link.Type == linkType && name == targetName {
return link, nil
}
if link.Type == proton.LinkTypeFolder {
childrenLinks, err := protonDrive.c.ListChildren(ctx, protonDrive.MainShare.ShareID, link.LinkID, true)
if err != nil {
return nil, err
}
// log.Printf("childrenLinks len = %v, %#v", len(childrenLinks), childrenLinks)
// get current node's keyring
linkKR, err := link.GetKeyRing(parentNodeKR, protonDrive.AddrKR)
if err != nil {
return nil, err
}
defer linkKR.ClearPrivateParams()
for _, childLink := range childrenLinks {
ret, err := protonDrive.searchByNameRecursively(ctx, linkKR, &childLink, targetName, linkType)
if err != nil {
return nil, err
}
if ret != nil {
return ret, nil
}
}
}
return nil, nil
}
// if the target isn't found, nil will be returned for both return values
func (protonDrive *ProtonDrive) SearchByNameInFolderByID(ctx context.Context,
folderLinkID string,
targetName string,
searchForFile, searchForFolder bool) (*proton.Link, error) {
folderLink, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, folderLinkID)
if err != nil {
return nil, err
}
return protonDrive.SearchByNameInFolder(ctx, &folderLink, targetName, searchForFile, searchForFolder)
}
func (protonDrive *ProtonDrive) SearchByNameInFolder(
ctx context.Context,
folderLink *proton.Link,
targetName string,
searchForFile, searchForFolder bool) (*proton.Link, error) {
if !searchForFile && !searchForFolder {
// nothing to search
return nil, nil
}
// we search all folders and files within this designated folder only
if folderLink.Type != proton.LinkTypeFolder {
return nil, ErrLinkTypeMustToBeFolderType
}
parentNodeKR, err := protonDrive.getNodeKRByID(ctx, folderLink.ParentLinkID)
if err != nil {
return nil, err
}
// get current node's keyring
folderLinkKR, err := folderLink.GetKeyRing(parentNodeKR, protonDrive.AddrKR)
if err != nil {
return nil, err
}
defer folderLinkKR.ClearPrivateParams()
childrenLinks, err := protonDrive.c.ListChildren(ctx, protonDrive.MainShare.ShareID, folderLink.LinkID, true)
if err != nil {
return nil, err
}
for _, childLink := range childrenLinks {
if childLink.State != proton.LinkStateActive {
// we only search in the active folders
continue
}
name, err := childLink.GetName(folderLinkKR, protonDrive.AddrKR)
if err != nil {
return nil, err
}
if searchForFile && childLink.Type == proton.LinkTypeFile && name == targetName {
return &childLink, nil
} else if searchForFolder && childLink.Type == proton.LinkTypeFolder && name == targetName {
return &childLink, nil
}
}
return nil, nil
}

25
shares.go Normal file
View File

@@ -0,0 +1,25 @@
package proton_api_bridge
import (
"context"
"github.com/henrybear327/go-proton-api"
)
func getAllShares(ctx context.Context, c *proton.Client) ([]proton.ShareMetadata, error) {
shares, err := c.ListShares(ctx, true)
if err != nil {
return nil, err
}
return shares, nil
}
func getShareByID(ctx context.Context, c *proton.Client, shareID string) (*proton.Share, error) {
share, err := c.GetShare(ctx, shareID)
if err != nil {
return nil, err
}
return &share, nil
}

0
testcase/empty.txt Normal file
View File

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

9
utility/init.go Normal file
View File

@@ -0,0 +1,9 @@
package utility
import (
"log"
)
func SetupLog() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
}

16
volumes.go Normal file
View File

@@ -0,0 +1,16 @@
package proton_api_bridge
import (
"context"
"github.com/henrybear327/go-proton-api"
)
func listAllVolumes(ctx context.Context, c *proton.Client) ([]proton.Volume, error) {
volumes, err := c.ListVolumes(ctx)
if err != nil {
return nil, err
}
return volumes, nil
}