mirror of
https://github.com/henrybear327/Proton-API-Bridge.git
synced 2026-05-19 04:16:09 -04:00
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
This commit is contained in:
10
README.md
10
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
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
264
drive_test.go
264
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{})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
4
error.go
4
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")
|
||||
)
|
||||
|
||||
114
file.go
114
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
2
go.mod
2
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
|
||||
)
|
||||
|
||||
|
||||
4
go.sum
4
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=
|
||||
|
||||
45
search.go
45
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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user