From 208b70cfd43b732eaa58302208d6a49a6d81f072 Mon Sep 17 00:00:00 2001 From: Chun-Hung Tseng Date: Thu, 29 Jun 2023 22:59:17 +0200 Subject: [PATCH] Improve/refactor file upload - when the file draft exists Add config option - ReplaceExistingDraft Refactor and add integration tests Update README and add notes for MIME parsing and progress bug Improve returned errors --- README.md | 10 +- common/config.go | 3 + delete.go | 2 +- drive_test.go | 264 +++++++++++++++++++++++-------------------- drive_test_helper.go | 114 +++++++++++++++---- error.go | 4 +- file.go | 114 ++++++++++++------- folder.go | 4 +- go.mod | 2 +- go.sum | 4 +- search.go | 45 +++++--- 11 files changed, 349 insertions(+), 217 deletions(-) diff --git a/README.md b/README.md index 5bf037f..04aee60 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,10 @@ V2 will bring in optimizations and enhancements, such as optimizing uploading an - [x] Point to the right proton-go-api branch - [x] Run `go get github.com/henrybear327/go-proton-api@dev` to update go mod - [x] Pass in AppVersion as a config option +- [x] Proper error handling by looking at the return code instead of the error string + - [x] Duplicated folder name handling: 422: A file or folder with that name already exists (Code=2500, Status=422) + - [x] Not found: ERROR RESTY 422: File or folder was not found. (Code=2501, Status=422), Attempt 1 + - [x] Failed upload: Draft already exists on this revision (Code=2500, Status=409) ### Known limitations @@ -99,15 +103,13 @@ V2 will bring in optimizations and enhancements, such as optimizing uploading an ## V2 +- [ ] Confirm the HMAC algorithm -> if you create a draft using integration test, and then use the web frontend to finish the upload (you will see overwrite pop-up), and then use the web frontend to upload again the same file, but this time you will have 2 files with duplicated names +- [ ] Fix file upload progress -> If the upload failed, please Replace file. If the upload is still in progress, replacing it will cancel the ongoing upload. - [ ] 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) - [ ] Documentation - [ ] Go through Drive iOS source code and check the logic control flow - [ ] Figure out the bottleneck by doing some profiling -- [ ] Proper error handling by looking at the return code instead of the error string - - [ ] Duplicated folder name handling: 422: A file or folder with that name already exists (Code=2500, Status=422) - - [ ] Not found: ERROR RESTY 422: File or folder was not found. (Code=2501, Status=422), Attempt 1 - - [ ] Failed upload: Draft already exists on this revision (Code=2500, Status=409) - [ ] File - [ ] Improve large file handling - [ ] Handle failed / interrupted upload diff --git a/common/config.go b/common/config.go index ad49fe9..07e6b3e 100644 --- a/common/config.go +++ b/common/config.go @@ -15,6 +15,7 @@ type Config struct { /* 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 + ReplaceExistingDraft bool // for the file upload replace or keep it as-is option /* Drive */ DataFolderName string @@ -53,6 +54,7 @@ func NewConfigWithDefaultValues() *Config { DestructiveIntegrationTest: false, EmptyTrashAfterIntegrationTest: false, + ReplaceExistingDraft: false, DataFolderName: "data", } @@ -94,6 +96,7 @@ func NewConfigForIntegrationTests() *Config { DestructiveIntegrationTest: true, EmptyTrashAfterIntegrationTest: true, + ReplaceExistingDraft: false, DataFolderName: "data", } diff --git a/delete.go b/delete.go index 4115d7e..f3fd40b 100644 --- a/delete.go +++ b/delete.go @@ -36,7 +36,7 @@ func (protonDrive *ProtonDrive) MoveFolderToTrashByID(ctx context.Context, linkI return ErrLinkTypeMustToBeFolderType } - childrenLinks, err := protonDrive.c.ListChildren(ctx, protonDrive.MainShare.ShareID, linkID, false) + childrenLinks, err := protonDrive.c.ListChildren(ctx, protonDrive.MainShare.ShareID, linkID /* false: list only active ones */, false) if err != nil { return err } diff --git a/drive_test.go b/drive_test.go index cd2ad71..e97dd8a 100644 --- a/drive_test.go +++ b/drive_test.go @@ -1,62 +1,15 @@ package proton_api_bridge import ( - "context" "log" "strings" "testing" - "github.com/henrybear327/Proton-API-Bridge/common" - "github.com/henrybear327/Proton-API-Bridge/utility" + "github.com/henrybear327/go-proton-api" ) -func setup(t *testing.T) (context.Context, context.CancelFunc, *ProtonDrive) { - utility.SetupLog() - - config := common.NewConfigForIntegrationTests() - - { - // pre-condition check - if !config.DestructiveIntegrationTest { - t.Fatalf("CAUTION: the integration test requires a clean proton drive") - } - if !config.EmptyTrashAfterIntegrationTest { - 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 TestCreateAndDeleteFolder(t *testing.T) { - ctx, cancel, protonDrive := setup(t) + ctx, cancel, protonDrive := setup(t, false) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) @@ -64,63 +17,132 @@ func TestCreateAndDeleteFolder(t *testing.T) { log.Println("Create a folder tmp at root") createFolder(t, ctx, protonDrive, "", "tmp") - checkFileListing(t, ctx, protonDrive, []string{"/tmp"}) + checkActiveFileListing(t, ctx, protonDrive, []string{"/tmp"}) - log.Println("Delet folder tmp") - deleteBySearchingFromRoot(t, ctx, protonDrive, "tmp", true) - checkFileListing(t, ctx, protonDrive, []string{}) + log.Println("Delete folder tmp") + deleteBySearchingFromRoot(t, ctx, protonDrive, "tmp", true, false) + checkActiveFileListing(t, ctx, protonDrive, []string{}) +} + +func TestCreateAndCreateAndDeleteFolder(t *testing.T) { + ctx, cancel, protonDrive := setup(t, false) + t.Cleanup(func() { + defer cancel() + defer tearDown(t, ctx, protonDrive) + }) + + log.Println("Create a folder tmp at root") + createFolder(t, ctx, protonDrive, "", "tmp") + checkActiveFileListing(t, ctx, protonDrive, []string{"/tmp"}) + + log.Println("Create a folder tmp at root again") + createFolderExpectError(t, ctx, protonDrive, "", "tmp", proton.ErrFolderNameExist) + checkActiveFileListing(t, ctx, protonDrive, []string{"/tmp"}) + + log.Println("Delete folder tmp") + deleteBySearchingFromRoot(t, ctx, protonDrive, "tmp", true, false) } func TestUploadAndDownloadAndDeleteAFile(t *testing.T) { - ctx, cancel, protonDrive := setup(t) + ctx, cancel, protonDrive := setup(t, true) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) }) log.Println("Upload integrationTestImage.png") - uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png") - checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1) - checkFileListing(t, ctx, protonDrive, []string{"/integrationTestImage.png"}) + uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png", false) + checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 1, 0, 0) + checkActiveFileListing(t, ctx, protonDrive, []string{"/integrationTestImage.png"}) downloadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png", "") log.Println("Upload a new revision to replace integrationTestImage.png") - uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage2.png") /* Add a revision */ - checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 2) + uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage2.png", false) /* Add a revision */ + checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 2, 1, 0, 1) downloadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage2.png", "") - checkFileListing(t, ctx, protonDrive, []string{"/integrationTestImage.png"}) log.Println("Delete file integrationTestImage.png") - deleteBySearchingFromRoot(t, ctx, protonDrive, "integrationTestImage.png", false) - checkFileListing(t, ctx, protonDrive, []string{}) + deleteBySearchingFromRoot(t, ctx, protonDrive, "integrationTestImage.png", false, false) + checkActiveFileListing(t, ctx, protonDrive, []string{}) +} + +func TestPartialUploadAndReuploadFailedAndDownloadAndDeleteAFile(t *testing.T) { + ctx, cancel, protonDrive := setup(t, false) + t.Cleanup(func() { + defer cancel() + defer tearDown(t, ctx, protonDrive) + }) + + log.Println("Partial upload a new draft revision of integrationTestImage.png") + uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png", true) + checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 0, 1, 0) + checkActiveFileListing(t, ctx, protonDrive, []string{}) + + log.Println("Partial upload a new draft revision of integrationTestImage.png again") + uploadFileByFilepathWithError(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png", true, ErrDraftExists) + checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 0, 1, 0) + checkActiveFileListing(t, ctx, protonDrive, []string{}) + + // FIXME: delete file with draft revision only + // log.Println("Delete file integrationTestImage.png") + // deleteBySearchingFromRoot(t, ctx, protonDrive, "integrationTestImage.png", false, true) + // checkActiveFileListing(t, ctx, protonDrive, []string{}) +} + +func TestPartialUploadAndReuploadAndDownloadAndDeleteAFile(t *testing.T) { + ctx, cancel, protonDrive := setup(t, true) + t.Cleanup(func() { + defer cancel() + defer tearDown(t, ctx, protonDrive) + }) + + log.Println("Partial upload a new draft revision of integrationTestImage.png") + uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png", true) + checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 0, 1, 0) + checkActiveFileListing(t, ctx, protonDrive, []string{}) + + log.Println("Partial upload a new draft revision of integrationTestImage.png again") + uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage.png", true) + checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 0, 1, 0) + checkActiveFileListing(t, ctx, protonDrive, []string{}) + + log.Println("Upload a new revision and activates it to replace integrationTestImage.png") + uploadFileByFilepath(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage2.png", false) /* Add a revision */ + checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 1, 0, 0) + downloadFile(t, ctx, protonDrive, "", "integrationTestImage.png", "testcase/integrationTestImage2.png", "") + checkActiveFileListing(t, ctx, protonDrive, []string{"/integrationTestImage.png"}) + + log.Println("Delete file integrationTestImage.png") + deleteBySearchingFromRoot(t, ctx, protonDrive, "integrationTestImage.png", false, false) + checkActiveFileListing(t, ctx, protonDrive, []string{}) } func TestUploadAndDeleteAnEmptyFileAtRoot(t *testing.T) { - ctx, cancel, protonDrive := setup(t) + ctx, cancel, protonDrive := setup(t, false) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) }) log.Println("Upload empty.txt") - uploadFileByFilepath(t, ctx, protonDrive, "", "empty.txt", "testcase/empty.txt") - checkRevisions(protonDrive, ctx, t, "empty.txt", 1) - checkFileListing(t, ctx, protonDrive, []string{"/empty.txt"}) + uploadFileByFilepath(t, ctx, protonDrive, "", "empty.txt", "testcase/empty.txt", false) + checkRevisions(protonDrive, ctx, t, "empty.txt", 1, 1, 0, 0) + checkActiveFileListing(t, ctx, protonDrive, []string{"/empty.txt"}) downloadFile(t, ctx, protonDrive, "", "empty.txt", "testcase/empty.txt", "") log.Println("Upload a new revision to replace empty.txt") - uploadFileByFilepath(t, ctx, protonDrive, "", "empty.txt", "testcase/empty.txt") /* Add a revision */ - checkRevisions(protonDrive, ctx, t, "empty.txt", 2) + uploadFileByFilepath(t, ctx, protonDrive, "", "empty.txt", "testcase/empty.txt", false) /* Add a revision */ + checkRevisions(protonDrive, ctx, t, "empty.txt", 2, 1, 0, 1) downloadFile(t, ctx, protonDrive, "", "empty.txt", "testcase/empty.txt", "") - checkFileListing(t, ctx, protonDrive, []string{"/empty.txt"}) + checkActiveFileListing(t, ctx, protonDrive, []string{"/empty.txt"}) log.Println("Delete file empty.txt") - deleteBySearchingFromRoot(t, ctx, protonDrive, "empty.txt", false) - checkFileListing(t, ctx, protonDrive, []string{}) + deleteBySearchingFromRoot(t, ctx, protonDrive, "empty.txt", false, false) + checkActiveFileListing(t, ctx, protonDrive, []string{}) } func TestUploadAndDownloadAndDeleteAFileAtAFolderOneLevelFromRoot(t *testing.T) { - ctx, cancel, protonDrive := setup(t) + ctx, cancel, protonDrive := setup(t, false) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) @@ -128,26 +150,26 @@ func TestUploadAndDownloadAndDeleteAFileAtAFolderOneLevelFromRoot(t *testing.T) log.Println("Create folder level1") createFolder(t, ctx, protonDrive, "", "level1") - checkFileListing(t, ctx, protonDrive, []string{"/level1"}) + checkActiveFileListing(t, ctx, protonDrive, []string{"/level1"}) log.Println("Upload integrationTestImage.png to level1") - uploadFileByFilepath(t, ctx, protonDrive, "level1", "integrationTestImage.png", "testcase/integrationTestImage.png") - checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1) - checkFileListing(t, ctx, protonDrive, []string{"/level1", "/level1/integrationTestImage.png"}) + uploadFileByFilepath(t, ctx, protonDrive, "level1", "integrationTestImage.png", "testcase/integrationTestImage.png", false) + checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 1, 0, 0) + checkActiveFileListing(t, ctx, protonDrive, []string{"/level1", "/level1/integrationTestImage.png"}) downloadFile(t, ctx, protonDrive, "level1", "integrationTestImage.png", "testcase/integrationTestImage.png", "") log.Println("Upload a new revision to replace integrationTestImage.png in level1") - uploadFileByFilepath(t, ctx, protonDrive, "level1", "integrationTestImage.png", "testcase/integrationTestImage2.png") /* Add a revision */ - checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 2) + uploadFileByFilepath(t, ctx, protonDrive, "level1", "integrationTestImage.png", "testcase/integrationTestImage2.png", false) /* Add a revision */ + checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 2, 1, 0, 1) downloadFile(t, ctx, protonDrive, "level1", "integrationTestImage.png", "testcase/integrationTestImage2.png", "") log.Println("Delete folder level1") - deleteBySearchingFromRoot(t, ctx, protonDrive, "level1", true) - checkFileListing(t, ctx, protonDrive, []string{}) + deleteBySearchingFromRoot(t, ctx, protonDrive, "level1", true, false) + checkActiveFileListing(t, ctx, protonDrive, []string{}) } func TestCreateAndMoveAndDeleteFolder(t *testing.T) { - ctx, cancel, protonDrive := setup(t) + ctx, cancel, protonDrive := setup(t, false) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) @@ -155,23 +177,23 @@ func TestCreateAndMoveAndDeleteFolder(t *testing.T) { log.Println("Create a folder src at root") createFolder(t, ctx, protonDrive, "", "src") - checkFileListing(t, ctx, protonDrive, []string{"/src"}) + checkActiveFileListing(t, ctx, protonDrive, []string{"/src"}) log.Println("Create a folder dst at root") createFolder(t, ctx, protonDrive, "", "dst") - checkFileListing(t, ctx, protonDrive, []string{"/src", "/dst"}) + checkActiveFileListing(t, ctx, protonDrive, []string{"/src", "/dst"}) log.Println("Move folder src to under folder dst") moveFolder(t, ctx, protonDrive, "src", "dst") - checkFileListing(t, ctx, protonDrive, []string{"/dst", "/dst/src"}) + checkActiveFileListing(t, ctx, protonDrive, []string{"/dst", "/dst/src"}) log.Println("Delete folder dst") - deleteBySearchingFromRoot(t, ctx, protonDrive, "dst", true) - checkFileListing(t, ctx, protonDrive, []string{}) + deleteBySearchingFromRoot(t, ctx, protonDrive, "dst", true, false) + checkActiveFileListing(t, ctx, protonDrive, []string{}) } func TestCreateAndMoveAndDeleteFolderWithAFile(t *testing.T) { - ctx, cancel, protonDrive := setup(t) + ctx, cancel, protonDrive := setup(t, false) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) @@ -179,35 +201,35 @@ func TestCreateAndMoveAndDeleteFolderWithAFile(t *testing.T) { log.Println("Create a folder src at root") createFolder(t, ctx, protonDrive, "", "src") - checkFileListing(t, ctx, protonDrive, []string{"/src"}) + checkActiveFileListing(t, ctx, protonDrive, []string{"/src"}) log.Println("Upload integrationTestImage.png to src") - uploadFileByFilepath(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage.png") - checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1) - checkFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png"}) + uploadFileByFilepath(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage.png", false) + checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 1, 0, 0) + checkActiveFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png"}) downloadFile(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage.png", "") log.Println("Create a folder dst at root") createFolder(t, ctx, protonDrive, "", "dst") - checkFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png", "/dst"}) + checkActiveFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png", "/dst"}) log.Println("Upload a new revision to replace integrationTestImage.png in src") - uploadFileByFilepath(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage2.png") /* Add a revision */ - checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 2) + uploadFileByFilepath(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage2.png", false) /* Add a revision */ + checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 2, 1, 0, 1) downloadFile(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage2.png", "") - checkFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png", "/dst"}) + checkActiveFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png", "/dst"}) log.Println("Move folder src to under folder dst") moveFolder(t, ctx, protonDrive, "src", "dst") - checkFileListing(t, ctx, protonDrive, []string{"/dst", "/dst/src", "/dst/src/integrationTestImage.png"}) + checkActiveFileListing(t, ctx, protonDrive, []string{"/dst", "/dst/src", "/dst/src/integrationTestImage.png"}) log.Println("Delete folder dst") - deleteBySearchingFromRoot(t, ctx, protonDrive, "dst", true) - checkFileListing(t, ctx, protonDrive, []string{}) + deleteBySearchingFromRoot(t, ctx, protonDrive, "dst", true, false) + checkActiveFileListing(t, ctx, protonDrive, []string{}) } func TestCreateAndMoveAndDeleteAFileOneLevelFromRoot(t *testing.T) { - ctx, cancel, protonDrive := setup(t) + ctx, cancel, protonDrive := setup(t, false) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) @@ -215,39 +237,39 @@ func TestCreateAndMoveAndDeleteAFileOneLevelFromRoot(t *testing.T) { log.Println("Create a folder src at root") createFolder(t, ctx, protonDrive, "", "src") - checkFileListing(t, ctx, protonDrive, []string{"/src"}) + checkActiveFileListing(t, ctx, protonDrive, []string{"/src"}) log.Println("Upload integrationTestImage.png to src") - uploadFileByFilepath(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage.png") - checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1) - checkFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png"}) + uploadFileByFilepath(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage.png", false) + checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 1, 1, 0, 0) + checkActiveFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png"}) downloadFile(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage.png", "") log.Println("Create a folder dst at root") createFolder(t, ctx, protonDrive, "", "dst") - checkFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png", "/dst"}) + checkActiveFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png", "/dst"}) log.Println("Upload a new revision to replace integrationTestImage.png in src") - uploadFileByFilepath(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage2.png") /* Add a revision */ - checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 2) + uploadFileByFilepath(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage2.png", false) /* Add a revision */ + checkRevisions(protonDrive, ctx, t, "integrationTestImage.png", 2, 1, 0, 1) downloadFile(t, ctx, protonDrive, "src", "integrationTestImage.png", "testcase/integrationTestImage2.png", "") - checkFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png", "/dst"}) + checkActiveFileListing(t, ctx, protonDrive, []string{"/src", "/src/integrationTestImage.png", "/dst"}) log.Println("Move file integrationTestImage.png to under folder dst") moveFile(t, ctx, protonDrive, "integrationTestImage.png", "dst") - checkFileListing(t, ctx, protonDrive, []string{"/src", "/dst", "/dst/integrationTestImage.png"}) + checkActiveFileListing(t, ctx, protonDrive, []string{"/src", "/dst", "/dst/integrationTestImage.png"}) log.Println("Delete folder dst") - deleteBySearchingFromRoot(t, ctx, protonDrive, "dst", true) - checkFileListing(t, ctx, protonDrive, []string{"/src"}) + deleteBySearchingFromRoot(t, ctx, protonDrive, "dst", true, false) + checkActiveFileListing(t, ctx, protonDrive, []string{"/src"}) log.Println("Delete folder src") - deleteBySearchingFromRoot(t, ctx, protonDrive, "src", true) - checkFileListing(t, ctx, protonDrive, []string{}) + deleteBySearchingFromRoot(t, ctx, protonDrive, "src", true, false) + checkActiveFileListing(t, ctx, protonDrive, []string{}) } func TestUploadLargeNumberOfBlocks(t *testing.T) { - ctx, cancel, protonDrive := setup(t) + ctx, cancel, protonDrive := setup(t, false) t.Cleanup(func() { defer cancel() defer tearDown(t, ctx, protonDrive) @@ -271,18 +293,18 @@ func TestUploadLargeNumberOfBlocks(t *testing.T) { file2ContentReader := strings.NewReader(file2Content) log.Println("Upload fileContent.txt") - uploadFileByReader(t, ctx, protonDrive, "", filename, file1ContentReader) - checkRevisions(protonDrive, ctx, t, filename, 1) - checkFileListing(t, ctx, protonDrive, []string{"/" + filename}) + uploadFileByReader(t, ctx, protonDrive, "", filename, file1ContentReader, false) + checkRevisions(protonDrive, ctx, t, filename, 1, 1, 0, 0) + checkActiveFileListing(t, ctx, protonDrive, []string{"/" + filename}) downloadFile(t, ctx, protonDrive, "", filename, "", file1Content) log.Println("Upload a new revision to replace fileContent.txt") - uploadFileByReader(t, ctx, protonDrive, "", filename, file2ContentReader) - checkRevisions(protonDrive, ctx, t, filename, 2) - checkFileListing(t, ctx, protonDrive, []string{"/" + filename}) + uploadFileByReader(t, ctx, protonDrive, "", filename, file2ContentReader, false) + checkRevisions(protonDrive, ctx, t, filename, 2, 1, 0, 1) + checkActiveFileListing(t, ctx, protonDrive, []string{"/" + filename}) downloadFile(t, ctx, protonDrive, "", filename, "", file2Content) log.Println("Delete file fileContent.txt") - deleteBySearchingFromRoot(t, ctx, protonDrive, filename, false) - checkFileListing(t, ctx, protonDrive, []string{}) + deleteBySearchingFromRoot(t, ctx, protonDrive, filename, false, false) + checkActiveFileListing(t, ctx, protonDrive, []string{}) } diff --git a/drive_test_helper.go b/drive_test_helper.go index 0495fba..873d178 100644 --- a/drive_test_helper.go +++ b/drive_test_helper.go @@ -9,11 +9,58 @@ import ( "testing" "time" + "github.com/henrybear327/Proton-API-Bridge/common" + "github.com/henrybear327/Proton-API-Bridge/utility" "github.com/henrybear327/go-proton-api" mathrand "math/rand" ) +func setup(t *testing.T, replaceExistingDraft bool) (context.Context, context.CancelFunc, *ProtonDrive) { + utility.SetupLog() + + config := common.NewConfigForIntegrationTests() + config.ReplaceExistingDraft = replaceExistingDraft + + { + // pre-condition check + if !config.DestructiveIntegrationTest { + t.Fatalf("CAUTION: the integration test requires a clean proton drive") + } + if !config.EmptyTrashAfterIntegrationTest { + 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) + } + } +} + // Taken from: https://github.com/rclone/rclone/blob/e43b5ce5e59b5717a9819ff81805dd431f710c10/lib/random/random.go // // StringFn create a random string for test purposes using the random @@ -44,12 +91,10 @@ func RandomString(n int) string { return StringFn(n, mathrand.Intn) } -/* Helper functions */ - -func createFolder(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string) { +func createFolderExpectError(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string, expectedError error) { parentLink := protonDrive.RootLink if parent != "" { - targetFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, parent, true) + targetFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, parent, true, false) if err != nil { t.Fatal(err) } @@ -63,15 +108,19 @@ func createFolder(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, p } _, err := protonDrive.CreateNewFolderByID(ctx, parentLink.LinkID, name) - if err != nil { + if err != expectedError { t.Fatal(err) } } -func uploadFileByReader(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string, in io.Reader) { +func createFolder(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string) { + createFolderExpectError(t, ctx, protonDrive, parent, name, nil) +} + +func uploadFileByReader(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string, in io.Reader, createFileOnly bool) { parentLink := protonDrive.RootLink if parent != "" { - targetFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, parent, true) + targetFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, parent, true, false) if err != nil { t.Fatal(err) } @@ -84,16 +133,16 @@ func uploadFileByReader(t *testing.T, ctx context.Context, protonDrive *ProtonDr t.Fatalf("parentLink is not of folder type") } - _, _, err := protonDrive.UploadFileByReader(ctx, parentLink.LinkID, name, time.Now(), in) + _, _, err := protonDrive.UploadFileByReader(ctx, parentLink.LinkID, name, time.Now(), in, createFileOnly) if err != nil { t.Fatal(err) } } -func uploadFileByFilepath(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string, filepath string) { +func uploadFileByFilepathWithError(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string, filepath string, createFileOnly bool, expectedError error) { parentLink := protonDrive.RootLink if parent != "" { - targetFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, parent, true) + targetFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, parent, true, false) if err != nil { t.Fatal(err) } @@ -119,16 +168,20 @@ func uploadFileByFilepath(t *testing.T, ctx context.Context, protonDrive *Proton in := bufio.NewReader(f) - _, _, err = protonDrive.UploadFileByReader(ctx, parentLink.LinkID, name, info.ModTime(), in) - if err != nil { + _, _, err = protonDrive.UploadFileByReader(ctx, parentLink.LinkID, name, info.ModTime(), in, createFileOnly) + if err != expectedError { t.Fatal(err) } } +func uploadFileByFilepath(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string, filepath string, createFileOnly bool) { + uploadFileByFilepathWithError(t, ctx, protonDrive, parent, name, filepath, createFileOnly, nil) +} + func downloadFile(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, parent, name string, filepath string, data string) { parentLink := protonDrive.RootLink if parent != "" { - targetFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, parent, true) + targetFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, parent, true, false) if err != nil { t.Fatal(err) } @@ -142,7 +195,7 @@ func downloadFile(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, p t.Fatalf("parentLink is not of folder type") } - targetFileLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, name, false) + targetFileLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, name, false, false) if err != nil { t.Fatal(err) } @@ -182,8 +235,8 @@ func downloadFile(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, p } } -func checkRevisions(protonDrive *ProtonDrive, ctx context.Context, t *testing.T, name string, totalRevisions int) { - targetFileLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, name, false) +func checkRevisions(protonDrive *ProtonDrive, ctx context.Context, t *testing.T, name string, totalRevisions, activeRevisions, draftRevisions, obseleteRevisions int) { + targetFileLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, name, false, true) if err != nil { t.Fatal(err) } @@ -198,12 +251,27 @@ func checkRevisions(protonDrive *ProtonDrive, ctx context.Context, t *testing.T, if len(revisions) != totalRevisions { t.Fatalf("Missing revision") } + + for i := range revisions { + if revisions[i].State == proton.RevisionStateActive { + activeRevisions-- + } + if revisions[i].State == proton.RevisionStateDraft { + draftRevisions-- + } + if revisions[i].State == proton.RevisionStateObsolete { + obseleteRevisions-- + } + } + if activeRevisions != 0 || draftRevisions != 0 || obseleteRevisions != 0 { + t.Fatalf("Wrong revision count %v %v %v", activeRevisions, draftRevisions, obseleteRevisions) + } } } // During the integration test, the name much be unique since the link is returned by recursively search for the name from root -func deleteBySearchingFromRoot(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, name string, isFolder bool) { - targetLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, name, isFolder) +func deleteBySearchingFromRoot(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, name string, isFolder bool, listAllActiveOrDraftFiles bool) { + targetLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, name, isFolder, listAllActiveOrDraftFiles) if err != nil { t.Fatal(err) } @@ -224,7 +292,7 @@ func deleteBySearchingFromRoot(t *testing.T, ctx context.Context, protonDrive *P } } -func checkFileListing(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, expectedPaths []string) { +func checkActiveFileListing(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, expectedPaths []string) { { paths := make([]string, 0) err := protonDrive.ListDirectoriesRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, false, -1, 0, true, "", &paths) @@ -270,11 +338,11 @@ func checkFileListing(t *testing.T, ctx context.Context, protonDrive *ProtonDriv } func moveFolder(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, srcFolderName, dstParentFolderName string) { - targetSrcFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, srcFolderName, true) + targetSrcFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, srcFolderName, true, false) if err != nil { t.Fatal(err) } - targetDestFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, dstParentFolderName, true) + targetDestFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, dstParentFolderName, true, false) if err != nil { t.Fatal(err) } @@ -289,11 +357,11 @@ func moveFolder(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, src } func moveFile(t *testing.T, ctx context.Context, protonDrive *ProtonDrive, srcFileName, dstParentFolderName string) { - targetSrcFileLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, srcFileName, false) + targetSrcFileLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, srcFileName, false, false) if err != nil { t.Fatal(err) } - targetDestFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, dstParentFolderName, true) + targetDestFolderLink, err := protonDrive.SearchByNameRecursivelyFromRoot(ctx, dstParentFolderName, true, false) if err != nil { t.Fatal(err) } diff --git a/error.go b/error.go index 2a7f64e..f3b957b 100644 --- a/error.go +++ b/error.go @@ -7,10 +7,12 @@ var ( 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") + ErrFolderIsNotEmpty = errors.New("folder can't be deleted because it is not empty") ErrInternalErrorOnFileUpload = errors.New("either link or createFileResp must be not nil") ErrMissingInputUploadAndCollectBlockData = errors.New("missing either session key or key ring") ErrLinkMustNotBeNil = errors.New("missing input proton link") ErrLinkMustBeActive = errors.New("can not operate on link state other than active") ErrDownloadedBlockHashVerificationFailed = errors.New("the hash of the downloaded block doesn't match the original hash") + ErrWrongGetRevisionUsage = errors.New("func GetRevision is used wrongly") + ErrDraftExists = errors.New("a draft exist - usually this means a file is being uploaded at another client, or, there was a failed upload attempt") ) diff --git a/file.go b/file.go index ee9e908..e2a9aa6 100644 --- a/file.go +++ b/file.go @@ -8,7 +8,6 @@ import ( "encoding/base64" "io" "os" - "strings" "time" "github.com/ProtonMail/gopenpgp/v2/crypto" @@ -31,7 +30,12 @@ func (protonDrive *ProtonDrive) DownloadFileByID(ctx context.Context, linkID str return protonDrive.DownloadFile(ctx, &link) } -func (protonDrive *ProtonDrive) GetActiveRevision(ctx context.Context, link *proton.Link) (*proton.Revision, error) { +func (protonDrive *ProtonDrive) GetRevision(ctx context.Context, link *proton.Link, revisionType proton.RevisionState) (*proton.RevisionMetadata, error) { + if revisionType != proton.RevisionStateActive && revisionType != proton.RevisionStateDraft { + // since we can't return more than 1 revision, we only support active and draft types + return nil, ErrWrongGetRevisionUsage + } + revisions, err := protonDrive.c.ListRevisions(ctx, protonDrive.MainShare.ShareID, link.LinkID) if err != nil { return nil, err @@ -39,21 +43,19 @@ func (protonDrive *ProtonDrive) GetActiveRevision(ctx context.Context, link *pro // 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 + // Each file can have 1 active/draft revision and n obsolete revisions. + targetRevision := -1 for i := range revisions { - if revisions[i].State == proton.RevisionStateActive { - activeRevision = i + if revisions[i].State == revisionType { + targetRevision = i + break } } - - revision, err := protonDrive.c.GetRevisionAllBlocks(ctx, protonDrive.MainShare.ShareID, link.LinkID, revisions[activeRevision].ID) - if err != nil { - return nil, err + if targetRevision == -1 { // not found + return nil, nil } - // log.Println("Total blocks", len(revision.Blocks)) - return &revision, nil + return &revisions[targetRevision], nil } func (protonDrive *ProtonDrive) GetActiveRevisionWithAttrs(ctx context.Context, link *proton.Link) (*proton.Revision, *FileSystemAttrs, error) { @@ -61,11 +63,17 @@ func (protonDrive *ProtonDrive) GetActiveRevisionWithAttrs(ctx context.Context, return nil, nil, ErrLinkMustNotBeNil } - revision, err := protonDrive.GetActiveRevision(ctx, link) + revisionMetadata, err := protonDrive.GetRevision(ctx, link, proton.RevisionStateActive) if err != nil { return nil, nil, err } + revision, err := protonDrive.c.GetRevisionAllBlocks(ctx, protonDrive.MainShare.ShareID, link.LinkID, revisionMetadata.ID) + if err != nil { + return nil, nil, err + } + // log.Println("Total blocks", len(revision.Blocks)) + nodeKR, err := protonDrive.getNodeKR(ctx, link) if err != nil { return nil, nil, err @@ -81,7 +89,7 @@ func (protonDrive *ProtonDrive) GetActiveRevisionWithAttrs(ctx context.Context, return nil, nil, err } - return revision, &FileSystemAttrs{ + return &revision, &FileSystemAttrs{ ModificationTime: modificationTime, Size: revisionXAttrCommon.Size, }, nil @@ -133,16 +141,16 @@ func (protonDrive *ProtonDrive) DownloadFile(ctx context.Context, link *proton.L return buffer.Bytes(), nil, nil } -func (protonDrive *ProtonDrive) UploadFileByReader(ctx context.Context, parentLinkID string, filename string, modTime time.Time, file io.Reader) (*proton.Link, int64, error) { +func (protonDrive *ProtonDrive) UploadFileByReader(ctx context.Context, parentLinkID string, filename string, modTime time.Time, file io.Reader, createFileOnly bool) (*proton.Link, int64, error) { parentLink, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, parentLinkID) if err != nil { return nil, 0, err } - return protonDrive.uploadFile(ctx, &parentLink, filename, modTime, file) + return protonDrive.uploadFile(ctx, &parentLink, filename, modTime, file, createFileOnly) } -func (protonDrive *ProtonDrive) UploadFileByPath(ctx context.Context, parentLink *proton.Link, filename string, filePath string) (*proton.Link, int64, error) { +func (protonDrive *ProtonDrive) UploadFileByPath(ctx context.Context, parentLink *proton.Link, filename string, filePath string, createFileOnly bool) (*proton.Link, int64, error) { f, err := os.Open(filePath) if err != nil { return nil, 0, err @@ -156,7 +164,7 @@ func (protonDrive *ProtonDrive) UploadFileByPath(ctx context.Context, parentLink in := bufio.NewReader(f) - return protonDrive.uploadFile(ctx, parentLink, filename, info.ModTime(), in) + return protonDrive.uploadFile(ctx, parentLink, filename, info.ModTime(), in, createFileOnly) } func (protonDrive *ProtonDrive) createFileUploadDraft(ctx context.Context, parentLink *proton.Link, filename string, modTime time.Time, mimeType string) (*proton.Link, *proton.CreateFileRes, *crypto.SessionKey, *crypto.KeyRing, error) { @@ -215,17 +223,14 @@ func (protonDrive *ProtonDrive) createFileUploadDraft(ctx context.Context, paren createFileResp, err := protonDrive.c.CreateFile(ctx, protonDrive.MainShare.ShareID, createFileReq) if err != nil { - // FIXME: check for duplicated filename by using checkAvailableHashes - // FIXME: better error handling - // 422: A file or folder with that name already exists (Code=2500, Status=422) - if strings.Contains(err.Error(), "(Code=2500, Status=422)") { - // file name conflict, file already exists - link, err := protonDrive.SearchByNameInFolder(ctx, parentLink, filename, true, false) + if err == proton.ErrFileNameExist { // FIXME: check for duplicated filename by relying on checkAvailableHashes + link, err := protonDrive.SearchByNameInFolder(ctx, parentLink, filename, true, false, true) // we search for everything with the requested name in the folder if err != nil { return nil, nil, nil, nil, err } return link, nil, nil, nil, nil } + // other real error caught return nil, nil, nil, nil, err } @@ -327,7 +332,6 @@ func (protonDrive *ProtonDrive) uploadAndCollectBlockData(ctx context.Context, n } func (protonDrive *ProtonDrive) commitNewRevision(ctx context.Context, nodeKR *crypto.KeyRing, modificationTime time.Time, size int64, manifestSignatureData []byte, blockTokens []proton.BlockToken, linkID, revisionID string) error { - // TODO: check iOS Drive CommitableRevision manifestSignature, err := protonDrive.AddrKR.SignDetached(crypto.NewPlainMessage(manifestSignatureData)) if err != nil { return err @@ -356,11 +360,13 @@ func (protonDrive *ProtonDrive) commitNewRevision(ctx context.Context, nodeKR *c return nil } -func (protonDrive *ProtonDrive) uploadFile(ctx context.Context, parentLink *proton.Link, filename string, modTime time.Time, file io.Reader) (*proton.Link, int64, error) { +func (protonDrive *ProtonDrive) uploadFile(ctx context.Context, parentLink *proton.Link, filename string, modTime time.Time, file io.Reader, createFileOnly bool) (*proton.Link, int64, error) { // FIXME: check iOS: optimize for large files -> enc blocks on the fly // main issue lies in the mimetype detection, since a full readout is used // detect MIME type + // FIXME: use https://pkg.go.dev/mime#ExtensionsByType + // FIXME: this will cause the upload progress to display the "fake" progress fileContent, err := io.ReadAll(file) if err != nil { return nil, 0, err @@ -383,13 +389,33 @@ func (protonDrive *ProtonDrive) uploadFile(ctx context.Context, parentLink *prot if link != nil { linkID = link.LinkID - // get a new revision - newRevision, err := protonDrive.c.CreateRevision(ctx, protonDrive.MainShare.ShareID, linkID) + draftRevision, err := protonDrive.GetRevision(ctx, link, proton.RevisionStateDraft) if err != nil { return nil, 0, err } + if draftRevision != nil { + if protonDrive.Config.ReplaceExistingDraft { + // FIXME: double check if this is the correct way of handling this case + // FIXME: how do we observe for file upload cancellation + revisionID = draftRevision.ID + } else { + // if there is a draft, based on the web behavior, it will ask if the user wants to replace the failed upload attempt + // current behavior, we report an error to not upload the file (conservative) + return nil, 0, ErrDraftExists + } + } else { + // get a new revision + newRevision, err := protonDrive.c.CreateRevision(ctx, protonDrive.MainShare.ShareID, linkID) + if err != nil { + if err == proton.ErrFileCanNotBeFound { + // Can happen when trying to create a revision on a file without an active revision + return nil, 0, err + } + return nil, 0, err + } - revisionID = newRevision.ID + revisionID = newRevision.ID + } // get newSessionKey and newNodeKR parentNodeKR, err := protonDrive.getNodeKRByID(ctx, link.ParentLinkID) @@ -408,33 +434,35 @@ func (protonDrive *ProtonDrive) uploadFile(ctx context.Context, parentLink *prot linkID = createFileResp.ID revisionID = createFileResp.RevisionID } else { - // might be the case where the upload failed, since file search will not include file with type draft + // should not happen anymore, since the file search will include the draft now return nil, 0, ErrInternalErrorOnFileUpload } + /* step 2: upload blocks and collect block data */ + manifestSignature := make([]byte, 0) + blockTokens := make([]proton.BlockToken, 0) if fileSize == 0 { - /* step 2: upload blocks and collect block data */ // skipped: no block to upload - - /* step 3: mark the file as active by updating the revision */ - manifestSignature := make([]byte, 0) - blockTokens := make([]proton.BlockToken, 0) - err = protonDrive.commitNewRevision(ctx, newNodeKR, modTime, fileSize, manifestSignature, blockTokens, linkID, revisionID) - if err != nil { - return nil, 0, err - } } else { - /* step 2: upload blocks and collect block data */ - manifestSignatureData, blockTokens, err := protonDrive.uploadAndCollectBlockData(ctx, newSessionKey, newNodeKR, fileContent, linkID, revisionID) + manifestSignature, blockTokens, err = protonDrive.uploadAndCollectBlockData(ctx, newSessionKey, newNodeKR, fileContent, linkID, revisionID) if err != nil { return nil, 0, err } + } - /* step 3: mark the file as active by updating the revision */ - err = protonDrive.commitNewRevision(ctx, newNodeKR, modTime, fileSize, manifestSignatureData, blockTokens, linkID, revisionID) + if createFileOnly { + // we try to simulate only draft is created but no upload is performed yet + finalLink, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, linkID) if err != nil { return nil, 0, err } + return &finalLink, fileSize, nil + } + + /* step 3: mark the file as active by updating the revision */ + err = protonDrive.commitNewRevision(ctx, newNodeKR, modTime, fileSize, manifestSignature, blockTokens, linkID, revisionID) + if err != nil { + return nil, 0, err } finalLink, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, linkID) diff --git a/folder.go b/folder.go index 34e5388..e80541b 100644 --- a/folder.go +++ b/folder.go @@ -167,8 +167,6 @@ func (protonDrive *ProtonDrive) CreateNewFolderByID(ctx context.Context, parentL } func (protonDrive *ProtonDrive) CreateNewFolder(ctx context.Context, parentLink *proton.Link, folderName string) (string, error) { - // TODO: check for duplicated folder name - parentNodeKR, err := protonDrive.getNodeKR(ctx, parentLink) if err != nil { return "", err @@ -218,11 +216,11 @@ func (protonDrive *ProtonDrive) CreateNewFolder(ctx context.Context, parentLink return "", err } + // if the folder name already exist, this call will return an error createFolderResp, err := protonDrive.c.CreateFolder(ctx, protonDrive.MainShare.ShareID, createFolderReq) if err != nil { return "", err } - // log.Printf("createFolderResp %#v", createFolderResp) return createFolderResp.ID, nil diff --git a/go.mod b/go.mod index d79bb0a..4f03bfd 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ 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-20230628220324-22ba21ecb67f + github.com/henrybear327/go-proton-api v0.0.0-20230630092102-8258d2e48d15 github.com/relvacode/iso8601 v1.3.0 ) diff --git a/go.sum b/go.sum index 13cec81..2de868d 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,8 @@ github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSM 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-20230628220324-22ba21ecb67f h1:OzLwkcwQZLkFnA0KpPVltWmw59Vq1XeJ9IzDjFvLdi4= -github.com/henrybear327/go-proton-api v0.0.0-20230628220324-22ba21ecb67f/go.mod h1:l42xBSOrCmkAxzWUHcoUsG/cP8m1hMhV72GoChOX3bg= +github.com/henrybear327/go-proton-api v0.0.0-20230630092102-8258d2e48d15 h1:RPH0WPXbBVNjPKru/t+KsYhbm3Sv82XeA3wPcv0+g1M= +github.com/henrybear327/go-proton-api v0.0.0-20230630092102-8258d2e48d15/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= diff --git a/search.go b/search.go index 3597b8d..e3c8777 100644 --- a/search.go +++ b/search.go @@ -11,17 +11,17 @@ import ( 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) { +func (protonDrive *ProtonDrive) SearchByNameRecursivelyFromRoot(ctx context.Context, targetName string, isFolder bool, listAllActiveOrDraftFiles 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) + return protonDrive.searchByNameRecursively(ctx, protonDrive.MainShareKR, protonDrive.RootLink, targetName, linkType, listAllActiveOrDraftFiles) } -func (protonDrive *ProtonDrive) SearchByNameRecursivelyByID(ctx context.Context, folderLinkID string, targetName string, isFolder bool) (*proton.Link, error) { +func (protonDrive *ProtonDrive) SearchByNameRecursivelyByID(ctx context.Context, folderLinkID string, targetName string, isFolder bool, listAllActiveOrDraftFiles bool) (*proton.Link, error) { folderLink, err := protonDrive.c.GetLink(ctx, protonDrive.MainShare.ShareID, folderLinkID) if err != nil { return nil, err @@ -41,10 +41,10 @@ func (protonDrive *ProtonDrive) SearchByNameRecursivelyByID(ctx context.Context, if err != nil { return nil, err } - return protonDrive.searchByNameRecursively(ctx, folderKeyRing, &folderLink, targetName, linkType) + return protonDrive.searchByNameRecursively(ctx, folderKeyRing, &folderLink, targetName, linkType, listAllActiveOrDraftFiles) } -func (protonDrive *ProtonDrive) SearchByNameRecursively(ctx context.Context, folderLink *proton.Link, targetName string, isFolder bool) (*proton.Link, error) { +func (protonDrive *ProtonDrive) SearchByNameRecursively(ctx context.Context, folderLink *proton.Link, targetName string, isFolder bool, listAllActiveOrDraftFiles bool) (*proton.Link, error) { var linkType proton.LinkType if isFolder { linkType = proton.LinkTypeFolder @@ -59,7 +59,7 @@ func (protonDrive *ProtonDrive) SearchByNameRecursively(ctx context.Context, fol if err != nil { return nil, err } - return protonDrive.searchByNameRecursively(ctx, folderKeyRing, folderLink, targetName, linkType) + return protonDrive.searchByNameRecursively(ctx, folderKeyRing, folderLink, targetName, linkType, listAllActiveOrDraftFiles) } func (protonDrive *ProtonDrive) searchByNameRecursively( @@ -67,12 +67,13 @@ func (protonDrive *ProtonDrive) searchByNameRecursively( parentNodeKR *crypto.KeyRing, link *proton.Link, targetName string, - linkType proton.LinkType) (*proton.Link, error) { - /* - Assumptions: - - we only care about the active ones - */ - if link.State != proton.LinkStateActive { + linkType proton.LinkType, + listAllActiveOrDraftFiles bool) (*proton.Link, error) { + if listAllActiveOrDraftFiles { + if link.State != proton.LinkStateActive && link.State != proton.LinkStateDraft { + return nil, nil + } + } else if link.State != proton.LinkStateActive { return nil, nil } @@ -100,7 +101,7 @@ func (protonDrive *ProtonDrive) searchByNameRecursively( defer linkKR.ClearPrivateParams() for _, childLink := range childrenLinks { - ret, err := protonDrive.searchByNameRecursively(ctx, linkKR, &childLink, targetName, linkType) + ret, err := protonDrive.searchByNameRecursively(ctx, linkKR, &childLink, targetName, linkType, listAllActiveOrDraftFiles) if err != nil { return nil, err } @@ -118,20 +119,20 @@ func (protonDrive *ProtonDrive) searchByNameRecursively( func (protonDrive *ProtonDrive) SearchByNameInFolderByID(ctx context.Context, folderLinkID string, targetName string, - searchForFile, searchForFolder bool) (*proton.Link, error) { + searchForFile, searchForFolder, listAllActiveOrDraftFiles 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) + return protonDrive.SearchByNameInFolder(ctx, &folderLink, targetName, searchForFile, searchForFolder, listAllActiveOrDraftFiles) } func (protonDrive *ProtonDrive) SearchByNameInFolder( ctx context.Context, folderLink *proton.Link, targetName string, - searchForFile, searchForFolder bool) (*proton.Link, error) { + searchForFile, searchForFolder, listAllActiveOrDraftFiles bool) (*proton.Link, error) { if !searchForFile && !searchForFolder { // nothing to search return nil, nil @@ -142,6 +143,11 @@ func (protonDrive *ProtonDrive) SearchByNameInFolder( return nil, ErrLinkTypeMustToBeFolderType } + if folderLink.State != proton.LinkStateActive { + // we only search in the active folders + return nil, nil + } + parentNodeKR, err := protonDrive.getNodeKRByID(ctx, folderLink.ParentLinkID) if err != nil { return nil, err @@ -159,8 +165,11 @@ func (protonDrive *ProtonDrive) SearchByNameInFolder( return nil, err } for _, childLink := range childrenLinks { - if childLink.State != proton.LinkStateActive { - // we only search in the active folders + if listAllActiveOrDraftFiles { + if childLink.State != proton.LinkStateActive && childLink.State != proton.LinkStateDraft { + continue + } + } else if childLink.State != proton.LinkStateActive { continue }