diff --git a/examples/.examples-ci.yml b/examples/.examples-ci.yml index c750032b..40ddfde2 100644 --- a/examples/.examples-ci.yml +++ b/examples/.examples-ci.yml @@ -11,6 +11,7 @@ go-sdk-examples: - intel - redact - vault + - share image: golang:${GO_VERSION} before_script: - export PANGEA_AUDIT_CONFIG_ID="${PANGEA_AUDIT_CONFIG_ID_1_LVE_AWS}" @@ -31,6 +32,7 @@ go-sdk-examples: - export PANGEA_URL_INTEL_TOKEN="${PANGEA_INTEGRATION_TOKEN_LVE_AWS}" - export PANGEA_USER_INTEL_TOKEN="${PANGEA_INTEGRATION_TOKEN_LVE_AWS}" - export PANGEA_VAULT_TOKEN="${PANGEA_INTEGRATION_TOKEN_LVE_AWS}" + - export PANGEA_SHARE_TOKEN="${PANGEA_INTEGRATION_TOKEN_LVE_AWS}" script: - cd examples/${EXAMPLE_FOLDER} - bash ../../dev/run_examples.sh diff --git a/examples/file_scan/file_scan_async_crowdstrike.go b/examples/file_scan/file_scan_async_crowdstrike.go index 3b90d1ba..ef1e7e29 100644 --- a/examples/file_scan/file_scan_async_crowdstrike.go +++ b/examples/file_scan/file_scan_async_crowdstrike.go @@ -58,16 +58,28 @@ func main() { } fmt.Println("Accepted error received (as expected).") - fmt.Println("Sleep some time before polling.") - // multiple polling attempts may be required - time.Sleep(time.Duration(20 * time.Second)) + var pr *pangea.PangeaResponse[any] + i := 0 + maxRetry := 24 - fmt.Println("File Scan poll result...") - pr, err := client.PollResultByError(ctx, *ae) - if err != nil { - log.Fatal(err) + fmt.Println("Let's try to poll result...") + for i < maxRetry { + // Wait for result + time.Sleep(time.Duration(10 * time.Second)) + + pr, err = client.PollResultByError(ctx, *ae) + if err == nil { + break + } + i++ + fmt.Printf("Result is not ready yet. Retry: %d\n", i) } - fmt.Println("File Scan poll result success.") - fmt.Println(pangea.Stringify(pr.Result)) + if i == maxRetry { + log.Fatal("Result still not ready") + } else { + r := (*pr.Result).(*file_scan.FileScanResult) + fmt.Println("File Scan success.") + fmt.Println(pangea.Stringify(r)) + } } diff --git a/examples/file_scan/file_scan_async_reversinglabs.go b/examples/file_scan/file_scan_async_reversinglabs.go index e0b81733..69567fc9 100644 --- a/examples/file_scan/file_scan_async_reversinglabs.go +++ b/examples/file_scan/file_scan_async_reversinglabs.go @@ -58,16 +58,28 @@ func main() { } fmt.Println("Accepted error received (as expected).") - fmt.Println("Sleep some time before polling.") - // multiple polling attempts may be required - time.Sleep(time.Duration(20 * time.Second)) + var pr *pangea.PangeaResponse[any] + i := 0 + maxRetry := 24 - fmt.Println("File Scan poll result...") - pr, err := client.PollResultByError(ctx, *ae) - if err != nil { - log.Fatal(err) + fmt.Println("Let's try to poll result...") + for i < maxRetry { + // Wait for result + time.Sleep(time.Duration(10 * time.Second)) + + pr, err = client.PollResultByError(ctx, *ae) + if err == nil { + break + } + i++ + fmt.Printf("Result is not ready yet. Retry: %d\n", i) } - fmt.Println("File Scan poll result success.") - fmt.Println(pangea.Stringify(pr.Result)) + if i == maxRetry { + log.Fatal("Result still not ready") + } else { + r := (*pr.Result).(*file_scan.FileScanResult) + fmt.Println("File Scan success.") + fmt.Println(pangea.Stringify(r)) + } } diff --git a/examples/share/folder_create_and_delete.go b/examples/share/folder_create_and_delete.go new file mode 100644 index 00000000..2ef8eff5 --- /dev/null +++ b/examples/share/folder_create_and_delete.go @@ -0,0 +1,61 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/pangeacyber/pangea-go/pangea-sdk/v3/pangea" + "github.com/pangeacyber/pangea-go/pangea-sdk/v3/service/share" +) + +func main() { + var t = time.Now().Format("20060102_150405") + var path = "/sdk_example/delete/" + t + + // Load pangea token from environment variables + token := os.Getenv("PANGEA_SHARE_TOKEN") + if token == "" { + log.Fatal("Unauthorized: No token present.") + } + + ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + // create a new store client with pangea token and domain + client := share.New(&pangea.Config{ + Token: token, + Domain: os.Getenv("PANGEA_DOMAIN"), + }) + + // Create a FolderCreateRequest and set the path of the folder to be created + input := &share.FolderCreateRequest{ + Path: path, + } + + fmt.Printf("Let's create a folder: %s\n", path) + // Send the CreateRequest + out, err := client.FolderCreate(ctx, input) + if err != nil { + log.Fatalf("unexpected error: %v", err) + } + + id := out.Result.Object.ID + fmt.Printf("Folder created. ID: %s.\n", id) + + fmt.Printf("Let's create this folder now\n") + // Create a DeleteRequest and set the ID of the item to be deleted + input2 := &share.DeleteRequest{ + ID: id, + } + + // Send the DeleteRequest + rDel, err := client.Delete(ctx, input2) + if err != nil { + log.Fatalf("unexpected error: %v", err) + } + + fmt.Printf("Folder deleted. Deleted %d items.\n", rDel.Result.Count) +} diff --git a/examples/share/go.mod b/examples/share/go.mod new file mode 100644 index 00000000..d70065b2 --- /dev/null +++ b/examples/share/go.mod @@ -0,0 +1,16 @@ +module examples/share + +go 1.19 + +require github.com/pangeacyber/pangea-go/pangea-sdk/v3 v3.7.0 + +require ( + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.5 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/rs/zerolog v1.31.0 // indirect + golang.org/x/sys v0.15.0 // indirect +) + +replace github.com/pangeacyber/pangea-go/pangea-sdk/v3 v3.7.0 => ../../pangea-sdk/v3 diff --git a/examples/share/go.sum b/examples/share/go.sum new file mode 100644 index 00000000..2982b94c --- /dev/null +++ b/examples/share/go.sum @@ -0,0 +1,30 @@ +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= +github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/examples/share/item_life_cycle.go b/examples/share/item_life_cycle.go new file mode 100644 index 00000000..cb7eae28 --- /dev/null +++ b/examples/share/item_life_cycle.go @@ -0,0 +1,236 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "path" + "time" + + "github.com/pangeacyber/pangea-go/pangea-sdk/v3/pangea" + "github.com/pangeacyber/pangea-go/pangea-sdk/v3/service/share" +) + +func main() { + var t = time.Now().Format("20060102_150405") + const filePath = "./testdata/testfile.pdf" + var folder = "/examples/files/" + t + + // Load pangea token from environment variables + token := os.Getenv("PANGEA_SHARE_TOKEN") + if token == "" { + log.Fatal("Unauthorized: No token present.") + } + + ctx, cancelFn := context.WithTimeout(context.Background(), 120*time.Second) + defer cancelFn() + + // create a new store client with pangea token and domain + fmt.Println("Creating new folder...") + client := share.New(&pangea.Config{ + Token: token, + Domain: os.Getenv("PANGEA_DOMAIN"), + QueuedRetryEnabled: true, + PollResultTimeout: 120 * time.Second, + Retry: true, + RetryConfig: &pangea.RetryConfig{ + RetryMax: 4, + }, + }) + + // Create a folder + respCreate, err := client.FolderCreate(ctx, &share.FolderCreateRequest{ + Path: folder, + }) + if err != nil { + log.Fatalf("unexpected error: %v", err) + } + + folderID := respCreate.Result.Object.ID + fmt.Printf("Folder create success. Folder ID: %s\n", folderID) + + // Upload a file with path as unique param + file, err := os.Open(filePath) + if err != nil { + log.Fatalf("unexpected error: %v", err) + } + + fmt.Println("Uploading file with Path field...") + respPut, err := client.Put(ctx, + &share.PutRequest{ + Path: path.Join(folder, "file_multipart_1"), + TransferRequest: pangea.TransferRequest{ + TransferMethod: pangea.TMmultipart, + }, + }, + file) + + if err != nil { + log.Fatalf("unexpected error: %v", err) + } + + fmt.Printf("Put file success. Object ID: %s\n", respPut.Result.Object.ID) + fmt.Printf("Parent ID: %s\n", respPut.Result.Object.ParentID) + + // Upload a file with parent id and name and adding metadata and tags + file, err = os.Open(filePath) + if err != nil { + log.Fatalf("unexpected error: %v", err) + } + + var metadata = map[string]string{"field1": "value1", "field2": "value2"} + var tags = []string{"tag1", "tag2"} + + fmt.Println("Uploading file with Name and ParentID...") + respPut2, err := client.Put(ctx, + &share.PutRequest{ + Name: "file_multipart_2", + ParentID: folderID, + TransferRequest: pangea.TransferRequest{ + TransferMethod: pangea.TMmultipart, + }, + Metadata: metadata, + Tags: tags, + }, + file) + if err != nil { + log.Fatalf("unexpected error: %v", err) + } + + fmt.Printf("Put file success. Object ID: %s\n", respPut2.Result.Object.ID) + + // Update file with full metadata and tags + fmt.Println("Updating object with metadata and tags...") + respUpdate, err := client.Update(ctx, &share.UpdateRequest{ + ID: respPut.Result.Object.ID, + Metadata: metadata, + Tags: tags, + }) + if err != nil { + log.Fatalf("unexpected error: %v", err) + } + + fmt.Printf("Updated item id %s successfully\n", respUpdate.Result.Object.ID) + + // Update file with add metadata and tags + fmt.Println("Adding metadata and tags to a object...") + var addMetadata = map[string]string{"field3": "value3"} + var addTags = []string{"tag3"} + + respUpdate2, err := client.Update(ctx, &share.UpdateRequest{ + ID: respPut2.Result.Object.ID, + AddMetadata: addMetadata, + AddTags: addTags, + }) + if err != nil { + log.Fatalf("unexpected error: %v", err) + } + fmt.Printf("Updated item id %s successfully\n", respUpdate2.Result.Object.ID) + + // Get archive as a multipart response + fmt.Println("Getting archive as multipart...") + respGetArchive, err := client.GetArchive(ctx, &share.GetArchiveRequest{ + Ids: []string{folderID}, + Format: share.AFzip, + TransferMethod: pangea.TMmultipart, + }) + + fmt.Printf("Archive download has %d file(s)\n", len(respGetArchive.AttachedFiles)) + for _, af := range respGetArchive.AttachedFiles { + // Save file. In this case should be just one archive anyway + err := af.Save(pangea.AttachedFileSaveInfo{ + Folder: "./download/archive/", + }) + if err != nil { + log.Fatalf("unexpected error: %v", err) + } + } + + // Get archive as a download url + fmt.Println("Getting archive as dest-url...") + respGetArchive2, err := client.GetArchive(ctx, &share.GetArchiveRequest{ + Ids: []string{folderID}, + Format: share.AFzip, + TransferMethod: pangea.TMdestURL, + }) + + fmt.Printf("Archive download has %d file(s)\n", len(respGetArchive2.AttachedFiles)) + + // Download file + fmt.Println("Download archive file from url...") + attachedFile, err := client.DownloadFile(ctx, *respGetArchive2.Result.DestURL) + if err != nil { + log.Fatalf("unexpected error: %v", err) + } + + fmt.Println("Download success. Saving file...") + + err = attachedFile.Save(pangea.AttachedFileSaveInfo{ + Folder: "./download/archive/", + }) + + if err != nil { + log.Fatalf("unexpected error: %v", err) + } + + fmt.Println("Save success") + + // Create share link... + fmt.Println("Creating share link...") + // Create authenticator methods to access the share link + authenticators := []share.Authenticator{share.Authenticator{ + AuthType: share.ATpassword, + AuthContext: "somepassword", + }} + + ll := []share.ShareLinkCreateItem{share.ShareLinkCreateItem{ + // Set targets to the share link + Targets: []string{folderID}, + LinkType: share.LTeditor, + Authenticators: authenticators, + MaxAccessCount: pangea.Int(3), + }} + respCreateLink, err := client.ShareLinkCreate(ctx, &share.ShareLinkCreateRequest{ + Links: ll, + }) + + links := respCreateLink.Result.ShareLinkObjects + link := links[0] + + fmt.Printf("Share link created: %s\n", link.Link) + + // Get share link + fmt.Println("Getting an already created share link...") + respGetLink, err := client.ShareLinkGet(ctx, &share.ShareLinkGetRequest{ + ID: link.ID, + }) + fmt.Printf("Get success: %s\n", respGetLink.Result.ShareLinkObject.Link) + + // List share link + fmt.Println("Getting a list of links...") + respListLink, err := client.ShareLinkList(ctx, &share.ShareLinkListRequest{}) + fmt.Printf("Got %d link(s)\n", respListLink.Result.Count) + + // Delete share link + fmt.Println("Deleting share link...") + respDeleteLink, err := client.ShareLinkDelete(ctx, &share.ShareLinkDeleteRequest{ + Ids: []string{link.ID}, + }) + + fmt.Printf("Deleted %d link(s)\n", len(respDeleteLink.Result.ShareLinkObjects)) + + // List files in folder + fmt.Println("Listing objects in folder...") + + // Create a ListFilter an set its possible values + listFilter := share.NewFilterList() + listFilter.Folder().Set(pangea.String(folder)) + + respList, err := client.List(ctx, &share.ListRequest{ + Filter: listFilter.Filter(), + }) + + fmt.Printf("Got %d object(s)\n", len(respList.Result.Objects)) + +} diff --git a/examples/share/put_split_upload.go b/examples/share/put_split_upload.go new file mode 100644 index 00000000..b5d42bb3 --- /dev/null +++ b/examples/share/put_split_upload.go @@ -0,0 +1,98 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/pangeacyber/pangea-go/pangea-sdk/v3/pangea" + "github.com/pangeacyber/pangea-go/pangea-sdk/v3/service/share" +) + +func main() { + var t = time.Now().Format("20060102_150405") + var name = "file_name_" + t + const filePath = "./testdata/testfile.pdf" + + // Load pangea token from environment variables + token := os.Getenv("PANGEA_SHARE_TOKEN") + if token == "" { + log.Fatal("Unauthorized: No token present.") + } + + ctx, cancelFn := context.WithTimeout(context.Background(), 120*time.Second) + defer cancelFn() + + // create a new store client with pangea token and domain + client := share.New(&pangea.Config{ + Token: token, + Domain: os.Getenv("PANGEA_DOMAIN"), + QueuedRetryEnabled: true, + PollResultTimeout: 120 * time.Second, + Retry: true, + RetryConfig: &pangea.RetryConfig{ + RetryMax: 4, + }, + }) + + // Create a PutRequest to request an presigned upload url. + // In this case TransferMethod is set to TMputURL + input := &share.PutRequest{ + Name: name, + TransferRequest: pangea.TransferRequest{ + TransferMethod: pangea.TMputURL, + }, + } + + file, err := os.Open(filePath) + if err != nil { + log.Fatalf("unexpected error: %v", err) + } + + resp, err := client.RequestUploadURL(ctx, input) + if err != nil { + log.Fatalf("unexpected error: %v", err.Error()) + } + + // Get presigned url + url := resp.AcceptedResult.PutURL + + fd := pangea.FileData{ + File: file, + Name: "someName", + } + + // Create an upload + uploader := pangea.NewFileUploader() + + // Upload the file to the url get previously + // Need to set transfer method again to TMputURL + err = uploader.UploadFile(ctx, url, pangea.TMputURL, fd) + if err != nil { + log.Fatalf("unexpected error: %v", err) + } + + var pr *pangea.PangeaResponse[any] + i := 0 + + // Try to poll result + for i < 24 { + // Wait until result should be ready + time.Sleep(time.Duration(10 * time.Second)) + + pr, err = client.PollResultByID(ctx, *resp.RequestID, &share.PutResult{}) + if err == nil { + break + } + i++ + } + + // Once got the result, cast it to use it + rPut := (*pr.Result).(*share.PutResult) + + fmt.Println("File uploaded:") + fmt.Printf("\tID: %s\n", rPut.Object.ID) + fmt.Printf("\tName: %s\n", rPut.Object.Name) +} diff --git a/examples/share/put_transfer_method_multipart.go b/examples/share/put_transfer_method_multipart.go new file mode 100644 index 00000000..fb208b90 --- /dev/null +++ b/examples/share/put_transfer_method_multipart.go @@ -0,0 +1,58 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/pangeacyber/pangea-go/pangea-sdk/v3/pangea" + "github.com/pangeacyber/pangea-go/pangea-sdk/v3/service/share" +) + +func main() { + var t = time.Now().Format("20060102_150405") + var name = "file_name_" + t + const filePath = "./testdata/testfile.pdf" + + // Load pangea token from environment variables + token := os.Getenv("PANGEA_SHARE_TOKEN") + if token == "" { + log.Fatal("Unauthorized: No token present.") + } + + ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + // create a new store client with pangea token and domain + client := share.New(&pangea.Config{ + Token: token, + Domain: os.Getenv("PANGEA_DOMAIN"), + QueuedRetryEnabled: true, + PollResultTimeout: 60 * time.Second, + }) + + // Create a PutRequest. In this case TransferMethod is set to TMpostURL + // So SDK is going to request a post url, upload the file to that url and then request to pangea for the /put result + input := &share.PutRequest{ + Name: name, + TransferRequest: pangea.TransferRequest{ + TransferMethod: pangea.TMmultipart, + }, + } + + file, err := os.Open(filePath) + if err != nil { + log.Fatalf("unexpected error: %v", err) + } + + rPut, err := client.Put(ctx, input, file) + if err != nil { + log.Fatalf("unexpected error: %v", err) + } + + fmt.Println("File uploaded:") + fmt.Printf("\tID: %s\n", rPut.Result.Object.ID) + fmt.Printf("\tName: %s\n", rPut.Result.Object.Name) +} diff --git a/examples/share/put_transfer_method_post_url.go b/examples/share/put_transfer_method_post_url.go new file mode 100644 index 00000000..a87ed80b --- /dev/null +++ b/examples/share/put_transfer_method_post_url.go @@ -0,0 +1,62 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "github.com/pangeacyber/pangea-go/pangea-sdk/v3/pangea" + "github.com/pangeacyber/pangea-go/pangea-sdk/v3/service/share" +) + +func main() { + var t = time.Now().Format("20060102_150405") + var name = "file_name_" + t + const filePath = "./testdata/testfile.pdf" + + // Load pangea token from environment variables + token := os.Getenv("PANGEA_SHARE_TOKEN") + if token == "" { + log.Fatal("Unauthorized: No token present.") + } + + ctx, cancelFn := context.WithTimeout(context.Background(), 120*time.Second) + defer cancelFn() + + // create a new store client with pangea token and domain + client := share.New(&pangea.Config{ + Token: token, + Domain: os.Getenv("PANGEA_DOMAIN"), + QueuedRetryEnabled: true, + PollResultTimeout: 120 * time.Second, + Retry: true, + RetryConfig: &pangea.RetryConfig{ + RetryMax: 4, + }, + }) + + // Create a PutRequest. In this case TransferMethod is set to TMpostURL + // So SDK is going to request a post url, upload the file to that url and then request to pangea for the /put result + input := &share.PutRequest{ + Name: name, + TransferRequest: pangea.TransferRequest{ + TransferMethod: pangea.TMpostURL, + }, + } + + file, err := os.Open(filePath) + if err != nil { + log.Fatalf("unexpected error: %v", err) + } + + rPut, err := client.Put(ctx, input, file) + if err != nil { + log.Fatalf("unexpected error: %v", err) + } + + fmt.Println("File uploaded:") + fmt.Printf("\tID: %s\n", rPut.Result.Object.ID) + fmt.Printf("\tName: %s\n", rPut.Result.Object.Name) +} diff --git a/examples/share/testdata/testfile.pdf b/examples/share/testdata/testfile.pdf new file mode 100644 index 00000000..26704774 Binary files /dev/null and b/examples/share/testdata/testfile.pdf differ diff --git a/pangea-sdk/.sdk-ci.yml b/pangea-sdk/.sdk-ci.yml index b78740f3..6a99d6a1 100644 --- a/pangea-sdk/.sdk-ci.yml +++ b/pangea-sdk/.sdk-ci.yml @@ -19,6 +19,7 @@ sdk-test-it: SERVICE_USER_INTEL_ENV: LVE SERVICE_REDACT_ENV: LVE SERVICE_VAULT_ENV: LVE + SERVICE_SHARE_ENV: LVE before_script: - echo $ENV - echo $CLOUD @@ -73,13 +74,12 @@ sdk-test-it: - CLOUD: [AWS, GCP] ENV: ${SERVICE_VAULT_ENV} TEST: vault + - CLOUD: [AWS, GCP] + ENV: ${SERVICE_SHARE_ENV} + TEST: share + allow_failure: false rules: - - if: $CI_COMMIT_BRANCH && '$CLOUD == "GCP" && $TEST == "file_scan"' - allow_failure: true - - if: $CI_COMMIT_BRANCH && '$CLOUD == "GCP" && $TEST != "file_scan"' - allow_failure: true - - if: $CI_COMMIT_BRANCH && '$CLOUD != "GCP"' - allow_failure: false + - if: $CI_COMMIT_BRANCH script: - go test -count=1 -tags integration -v ./service/${TEST}/... diff --git a/pangea-sdk/v3/pangea/file_uploader.go b/pangea-sdk/v3/pangea/file_uploader.go new file mode 100644 index 00000000..b5d689a1 --- /dev/null +++ b/pangea-sdk/v3/pangea/file_uploader.go @@ -0,0 +1,33 @@ +package pangea + +import ( + "context" + "fmt" +) + +type FileUploader struct { + client *Client +} + +func NewFileUploader() FileUploader { + cfg := &Config{ + QueuedRetryEnabled: false, + } + + return FileUploader{ + client: NewClient("FileUploader", cfg), + } +} + +func (fu *FileUploader) UploadFile(ctx context.Context, url string, tm TransferMethod, fd FileData) error { + if tm == TMmultipart { + return fmt.Errorf("%s is not supported in UploadFile. Use service client instead", tm) + } + + fds := FileData{ + File: fd.File, + Name: "file", + Details: fd.Details, + } + return fu.client.UploadFile(ctx, url, tm, fds) +} diff --git a/pangea-sdk/v3/pangea/pangea.go b/pangea-sdk/v3/pangea/pangea.go index 2d6412ec..c62e569a 100644 --- a/pangea-sdk/v3/pangea/pangea.go +++ b/pangea-sdk/v3/pangea/pangea.go @@ -321,6 +321,10 @@ func (c *Client) GetPresignedURL(ctx context.Context, url string, input any) (*R Interface("result", pr.RawResult). Send() + if *pr.Status == "Success" { // If this request already success, just return. Not need to poll presigned URL + return pr, nil, nil + } + err = c.CheckResponse(pr, &AcceptedResult{}) var ae *AcceptedError var ok bool @@ -436,28 +440,30 @@ func (c *Client) FullPostPresignedURL(ctx context.Context, url string, input Con return nil, err } - fds := FileData{ - File: fd.File, - Name: fd.Name, - Details: ar.PostFormData, - } + if ar != nil { // This is the case that GetPresignedURL return an already success response + fds := FileData{ + File: fd.File, + Name: fd.Name, + Details: ar.PostFormData, + } - err = c.UploadFile(ctx, ar.PostURL, TMpostURL, fds) - if err != nil { - c.Logger.Error(). - Str("service", c.serviceName). - Str("method", "PostPresignedURL.PostPresignedURL"). - Err(err) - return nil, err - } + err = c.UploadFile(ctx, ar.PostURL, TMpostURL, fds) + if err != nil { + c.Logger.Error(). + Str("service", c.serviceName). + Str("method", "PostPresignedURL.PostPresignedURL"). + Err(err) + return nil, err + } - pr, err = c.handledQueued(ctx, pr) - if err != nil { - c.Logger.Error(). - Str("service", c.serviceName). - Str("method", "PostPresignedURL.handleQueued"). - Err(err) - return nil, err + pr, err = c.handledQueued(ctx, pr) + if err != nil { + c.Logger.Error(). + Str("service", c.serviceName). + Str("method", "PostPresignedURL.handleQueued"). + Err(err) + return nil, err + } } err = c.CheckResponse(pr, out) @@ -870,8 +876,7 @@ func (c *Client) CheckResponse(r *Response, v any) error { switch v.(type) { case nil: // This should never be fired to user because Client is to internal use - err := fmt.Errorf("not initialized struct. Can't unmarshal result from response") - return err + return fmt.Errorf("not initialized struct. Can't unmarshal result from response") default: err := r.UnmarshalResult(v) if err != nil { diff --git a/pangea-sdk/v3/pangea/response.go b/pangea-sdk/v3/pangea/response.go index d21ef782..fa63cb9a 100644 --- a/pangea-sdk/v3/pangea/response.go +++ b/pangea-sdk/v3/pangea/response.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "path" + "path/filepath" pu "github.com/pangeacyber/pangea-go/pangea-sdk/v3/internal/pangeautil" ) @@ -143,7 +144,7 @@ func (af AttachedFile) Save(info AttachedFileSaveInfo) error { folder = info.Folder } - filename := "defaultFilename" + filename := "defaultSaveFilename" if af.Filename != "" { filename = af.Filename } @@ -151,7 +152,7 @@ func (af AttachedFile) Save(info AttachedFileSaveInfo) error { filename = info.Filename } - if _, err := os.Stat(folder); os.IsNotExist(err) { + if !fileExists(folder) { // Directory does not exist, create it err := os.MkdirAll(folder, os.ModePerm) if err != nil { @@ -160,6 +161,7 @@ func (af AttachedFile) Save(info AttachedFileSaveInfo) error { } filePath := path.Join(folder, filename) + filePath = findAvailableFile(filePath) err := os.WriteFile(filePath, af.File, 0644) if err != nil { @@ -169,6 +171,32 @@ func (af AttachedFile) Save(info AttachedFileSaveInfo) error { return nil } +func fileExists(filePath string) bool { + _, err := os.Stat(filePath) + return !os.IsNotExist(err) +} + +func findAvailableFile(filePath string) string { + if !fileExists(filePath) { + return filePath + } + + // Split the file path into directory, file name, and extension + dir, file := filepath.Split(filePath) + ext := filepath.Ext(file) + fileNameWithoutExt := file[:len(file)-len(ext)] + + counter := 1 + // Construct the new file path format + newFilePath := filepath.Join(dir, fmt.Sprintf("%s_%d%s", fileNameWithoutExt, counter, ext)) + for fileExists(newFilePath) { + counter++ + newFilePath = filepath.Join(dir, fmt.Sprintf("%s_%d%s", fileNameWithoutExt, counter, ext)) + } + + return newFilePath +} + // MarshalJSON implements the json.Marshaler interface for CustomType. func (r Response) MarshalJSON() ([]byte, error) { if r.rawResponse == nil { diff --git a/pangea-sdk/v3/pangea/utils.go b/pangea-sdk/v3/pangea/utils.go index 361faa3e..29a28006 100644 --- a/pangea-sdk/v3/pangea/utils.go +++ b/pangea-sdk/v3/pangea/utils.go @@ -5,6 +5,11 @@ import ( "crypto/sha256" "crypto/sha512" "encoding/hex" + "hash/crc32" + "io" + "os" + "strconv" + "strings" ) func HashSHA256(i string) string { @@ -25,3 +30,77 @@ func HashSHA512(i string) string { func GetHashPrefix(h string, len uint) string { return h[0:len] } + +type UploadFileParams struct { + Size int `json:"size,omitempty"` + CRC string `json:"crc32c,omitempty"` + SHA256 string `json:"sha256,omitempty"` +} + +func GetUploadFileParams(file *os.File) (*UploadFileParams, error) { + // Create a new CRC32C hash + crcHash := crc32.New(crc32.MakeTable(crc32.Castagnoli)) + // Create a new SHA256 hash + sha256Hash := sha256.New() + + // Seek back to the beginning of the file + _, err := file.Seek(0, 0) + if err != nil { + return nil, err + } + + // Copy the file content into the hash function + if _, err := io.Copy(sha256Hash, file); err != nil { + return nil, err + } + // Get the hash sum as a byte slice + hashInBytes := sha256Hash.Sum(nil) + + // Seek back to the beginning of the file + _, err = file.Seek(0, 0) + if err != nil { + return nil, err + } + + // Copy the file content into the hash calculation + size, err := io.Copy(crcHash, file) + if err != nil { + return nil, err + } + // Get the CRC32C checksum value + crc32c := crcHash.Sum32() + + // Reset to be sent + file.Seek(0, 0) + + // Convert the CRC32 value to hexadecimal + crcStr := strconv.FormatUint(uint64(crc32c), 16) + // Pad "0" on the left to make it 8 characters long. It's for the zero bytes file case + paddedCRCStr := strings.Repeat("0", 8-len(crcStr)) + crcStr + + return &UploadFileParams{ + CRC: paddedCRCStr, + SHA256: hex.EncodeToString(hashInBytes), + Size: int(size), + }, nil +} + +func GetFileSize(file *os.File) (int64, error) { + _, err := file.Seek(0, io.SeekStart) + if err != nil { + return 0, err + } + + // Seek to the end of the file + size, err := file.Seek(0, io.SeekEnd) + if err != nil { + return 0, err + } + + // Reset to be sent + file.Seek(0, io.SeekStart) + if err != nil { + return 0, err + } + return size, nil +} diff --git a/pangea-sdk/v3/service/file_scan/api.go b/pangea-sdk/v3/service/file_scan/api.go index 58a2dcf1..0c469ea7 100644 --- a/pangea-sdk/v3/service/file_scan/api.go +++ b/pangea-sdk/v3/service/file_scan/api.go @@ -103,6 +103,7 @@ type FileScanRequest struct { SourceURL string `json:"source_url,omitempty"` } +// NOTE: Will be deprecated in next major update in favor of pangea.UploadFileParams type FileScanFileParams struct { Size int `json:"size,omitempty"` CRC string `json:"crc32c,omitempty"` @@ -134,6 +135,7 @@ type FileScanResult struct { RawData interface{} `json:"raw_data,omitempty"` } +// NOTE: Will be deprecated in next major update in favor of pangea.GetUploadFileParams(file *os.File) (*UploadFileParams, error) func GetUploadFileParams(file *os.File) (*FileScanFileParams, error) { // Create a new CRC32C hash crcHash := crc32.New(crc32.MakeTable(crc32.Castagnoli)) diff --git a/pangea-sdk/v3/service/share/api.go b/pangea-sdk/v3/service/share/api.go new file mode 100644 index 00000000..b3112d95 --- /dev/null +++ b/pangea-sdk/v3/service/share/api.go @@ -0,0 +1,849 @@ +package share + +import ( + "context" + "errors" + "os" + + "github.com/pangeacyber/pangea-go/pangea-sdk/v3/internal/request" + "github.com/pangeacyber/pangea-go/pangea-sdk/v3/pangea" +) + +type ItemOrder string + +const ( + IOasc ItemOrder = "asc" + IOdes = "desc" +) + +type ArchiveFormat string + +const ( + AFzip ArchiveFormat = "zip" + AFtar = "tar" +) + +type LinkType string + +const ( + LTupload LinkType = "upload" + LTdownload = "download" + LTeditor = "editor" +) + +type AuthenticatorType string + +const ( + ATemailOTP AuthenticatorType = "email_otp" + ATpassword = "password" + ATsmsOTP = "sms_otp" + ATsocial = "social" +) + +type ObjectOrderBy string + +const ( + OOBid ObjectOrderBy = "id" + OOBcreatedAt = "created_at" + OOBname = "name" + OOBparendID = "parent_id" + OOBtype = "type" + OOBupdatedAt = "updated_at" +) + +type ShareLinkOrderBy string + +const ( + SLOBid ShareLinkOrderBy = "id" + SLOBstoragePoolID = "storage_pool_id" + SLOBtarget = "target" + SLOBlinkType = "link_type" + SLOBaccessCount = "access_count" + SLOBmaxAccessCount = "max_access_count" + SLOBcreatedAt = "created_at" + SLOBexpiresAt = "expires_at" + SLOBlastAccessedAt = "last_accessed_at" + SLOBlink = "link" +) + +type Metadata map[string]string +type Tags []string + +type DeleteRequest struct { + pangea.BaseRequest + + ID string `json:"id,omitempty"` + Force *bool `json:"force,omitempty"` + Path string `json:"path,omitempty"` +} + +type ItemData struct { + ID string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + Size int `json:"size"` + BillableSize int `json:"billable_size"` + Location string `json:"location"` + Tags Tags `json:"tags,omitempty"` + Metadata Metadata `json:"metadata,omitempty"` + MD5 string `json:"md5"` + SHA256 string `json:"sha256"` + SHA512 string `json:"sha512"` + ParentID string `json:"parent_id"` +} + +type DeleteResult struct { + Count int `json:"count"` +} + +// @summary Delete +// +// @description Delete object by ID or path. If both are supplied, the path must +// match that of the object represented by the ID. Beta API. +// +// @operationId store_post_v1beta_delete +// +// @example +// +// input := &share.DeleteRequest{ +// ID: "pos_3djfmzg2db4c6donarecbyv5begtj2bm" +// } +// +// res, err := shareClient.Delete(ctx, input) +func (e *share) Delete(ctx context.Context, input *DeleteRequest) (*pangea.PangeaResponse[DeleteResult], error) { + return request.DoPost(ctx, e.Client, "v1beta/delete", input, &DeleteResult{}) +} + +type FolderCreateRequest struct { + pangea.BaseRequest + + Name string `json:"name,omitempty"` + Metadata Metadata `json:"metadata,omitempty"` + ParentID string `json:"parent_id,omitempty"` + Path string `json:"path,omitempty"` + Tags Tags `json:"tags,omitempty"` +} + +type FolderCreateResult struct { + Object ItemData `json:"object"` +} + +// @summary Create a folder +// +// @description Create a folder, either by name or path and parent_id. Beta API. +// +// @operationId store_post_v1beta_folder_create +// +// @example +// +// input := &share.FolderCreateRequest{ +// Metadata: share.Metadata{ +// "created_by": "jim", +// "priority": "medium", +// }, +// ParentID: "pos_3djfmzg2db4c6donarecbyv5begtj2bm", +// Path: "/", +// Tags: share.Tags{"irs_2023", "personal"}, +// } +// +// res, err := shareClient.FolderCreate(ctx, input) +func (e *share) FolderCreate(ctx context.Context, input *FolderCreateRequest) (*pangea.PangeaResponse[FolderCreateResult], error) { + return request.DoPost(ctx, e.Client, "v1beta/folder/create", input, &FolderCreateResult{}) +} + +type GetRequest struct { + pangea.BaseRequest + + ID string `json:"id,omitempty"` + Path string `json:"path,omitempty"` + TransferMethod pangea.TransferMethod `json:"transfer_method,omitempty"` +} + +type GetResult struct { + Object ItemData `json:"object"` + DestURL *string `json:"dest_url,omitempty"` +} + +// @summary Get an object +// +// @description Get object. If both ID and path are supplied, the call will fail +// if the target object doesn't match both properties. Beta API. +// +// @operationId store_post_v1beta_get +// +// @example +// +// input := &share.GetRequest{ +// ID: "pos_3djfmzg2db4c6donarecbyv5begtj2bm", +// Path: "/", +// } +// +// res, err := shareClient.Get(ctx, input) +func (e *share) Get(ctx context.Context, input *GetRequest) (*pangea.PangeaResponse[GetResult], error) { + return request.DoPost(ctx, e.Client, "v1beta/get", input, &GetResult{}) +} + +type PutRequest struct { + pangea.BaseRequest + pangea.TransferRequest + + Name string `json:"name,omitempty"` + Format *FileFormat `json:"format,omitempty"` + Metadata Metadata `json:"metadata,omitempty"` + MimeType string `json:"mimetype,omitempty"` + ParentID string `json:"parent_id,omitempty"` + Path string `json:"path,omitempty"` + CRC32C string `json:"crc32c,omitempty"` + MD5 string `json:"md5,omitempty"` + SHA1 string `json:"sha1,omitempty"` + SHA256 string `json:"sha256,omitempty"` + SHA512 string `json:"sha512,omitempty"` + Size *int `json:"size,omitempty"` + Tags Tags `json:"tags,omitempty"` +} + +type PutResult struct { + Object ItemData `json:"object"` +} + +// @summary Upload a file +// +// @description Upload a file. Beta API. +// +// @operationId store_post_v1beta_put +// +// @example +// +// input := &share.PutRequest{ +// TransferMethod: pangea.TMmultipart, +// Metadata: share.Metadata{ +// "created_by": "jim", +// "priority": "medium", +// }, +// ParentID: "pos_3djfmzg2db4c6donarecbyv5begtj2bm", +// Path: "/", +// Tags: share.Tags{"irs_2023", "personal"}, +// } +// +// file, err := os.Open("./path/to/file.pdf") +// if err != nil { +// log.Fatal("Error opening file: %v", err) +// } +// +// res, err := shareClient.Put(ctx, input, file) +func (e *share) Put(ctx context.Context, input *PutRequest, file *os.File) (*pangea.PangeaResponse[PutResult], error) { + if input == nil { + return nil, errors.New("nil input") + } + + if input.TransferMethod == pangea.TMpostURL { + var err error + params, err := pangea.GetUploadFileParams(file) + if err != nil { + return nil, err + } + input.CRC32C = params.CRC + input.SHA256 = params.SHA256 + input.Size = pangea.Int(params.Size) + } else if size, err := pangea.GetFileSize(file); err == nil && size == 0 { + input.Size = pangea.Int(0) + } + + name := "file" + if input.TransferMethod == pangea.TMmultipart { + name = "upload" + } + + fd := pangea.FileData{ + File: file, + Name: name, + } + + return request.DoPostWithFile(ctx, e.Client, "v1beta/put", input, &PutResult{}, fd) +} + +type UpdateRequest struct { + pangea.BaseRequest + + ID string `json:"id"` + Path string `json:"path,omitempty"` + AddMetadata Metadata `json:"add_metadata,omitempty"` + RemoveMetadata Metadata `json:"remove_metadata,omitempty"` + Metadata Metadata `json:"metadata,omitempty"` + AddTags Tags `json:"add_tags,omitempty"` + RemoveTags Tags `json:"remove_tags,omitempty"` + Tags Tags `json:"tags,omitempty"` + ParentID string `json:"parent_id,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` +} + +type UpdateResult struct { + Object ItemData `json:"object"` +} + +// @summary Update a file +// +// @description Update a file. Beta API. +// +// @operationId share_post_v1beta_update +// +// @example +// +// input := &share.UpdateRequest{ +// ID: "pos_3djfmzg2db4c6donarecbyv5begtj2bm", +// Path: "/", +// RemoveMetadata: share.Metadata{ +// "created_by": "jim", +// "priority": "medium", +// }, +// RemoveTags: share.Tags{"irs_2023", "personal"}, +// } +// +// res, err := shareClient.Update(ctx, input) +func (e *share) Update(ctx context.Context, input *UpdateRequest) (*pangea.PangeaResponse[UpdateResult], error) { + return request.DoPost(ctx, e.Client, "v1beta/update", input, &UpdateResult{}) +} + +// Just allowed to filter by folder now +type FilterList struct { + pangea.FilterBase + folder *pangea.FilterMatch[string] +} + +func NewFilterList() *FilterList { + filter := make(pangea.Filter) + return &FilterList{ + FilterBase: *pangea.NewFilterBase(filter), + folder: pangea.NewFilterMatch[string]("folder", &filter), + } +} + +func (f *FilterList) Folder() *pangea.FilterMatch[string] { + return f.folder +} + +type ListRequest struct { + pangea.BaseRequest + + Filter pangea.Filter `json:"filter,omitempty"` + Last string `json:"last,omitempty"` + Order ItemOrder `json:"order,omitempty"` + OrderBy ObjectOrderBy `json:"order_by,omitempty"` + Size int `json:"size,omitempty"` +} + +type ListResult struct { + Count int `json:"count"` + Last string `json:"last,omitempty"` + Objects []ItemData `json:"objects"` +} + +// @summary List +// +// @description List or filter/search records. Beta. API +// +// @operationId share_post_v1beta_list +// +// @example +// +// input := &share.ListRequest{} +// +// res, err := shareClient.List(ctx, input) +func (e *share) List(ctx context.Context, input *ListRequest) (*pangea.PangeaResponse[ListResult], error) { + return request.DoPost(ctx, e.Client, "v1beta/list", input, &ListResult{}) +} + +type GetArchiveRequest struct { + pangea.BaseRequest + + Ids []string `json:"ids"` + Format ArchiveFormat `json:"format,omitempty"` + TransferMethod pangea.TransferMethod `json:"transfer_method,omitempty"` +} + +type GetArchiveResult struct { + DestURL *string `json:"dest_url,omitempty"` + Count int `json:"count"` +} + +// @summary Get archive +// +// @description Get an archive file of multiple objects. Beta API. +// +// @operationId share_post_v1beta_get_archive +// +// @example +// +// input := &share.GetArchiveRequest{ +// Ids: []string{"pos_3djfmzg2db4c6donarecbyv5begtj2bm"}, +// } +// +// res, err := shareClient.GetArchive(ctx, input) +func (e *share) GetArchive(ctx context.Context, input *GetArchiveRequest) (*pangea.PangeaResponse[GetArchiveResult], error) { + return request.DoPost(ctx, e.Client, "v1beta/get_archive", input, &GetArchiveResult{}) +} + +type Authenticator struct { + AuthType AuthenticatorType `json:"auth_type"` + AuthContext string `json:"auth_context"` +} + +type ShareLinkCreateItem struct { + Targets []string `json:"targets"` + LinkType LinkType `json:"link_type,omitempty"` + ExpiresAt string `json:"expires_at,omitempty"` + MaxAccessCount *int `json:"max_access_count,omitempty"` + Authenticators []Authenticator `json:"authenticators,omitempty"` + Message string `json:"message,omitempty"` + Title string `json:"title,omitempty"` + NotifyEmail string `json:"notify_email,omitempty"` + Tags Tags `json:"tags,omitempty"` +} + +type ShareLinkCreateRequest struct { + pangea.BaseRequest + + Links []ShareLinkCreateItem `json:"links"` +} + +type ShareLinkItem struct { + ID string `json:"id"` + StoragePoolID string `json:"storage_pool_id"` + Targets []string `json:"targets"` + LinkType string `json:"link_type"` + AccessCount int `json:"access_count"` + MaxAccessCount int `json:"max_access_count"` + CreatedAt string `json:"created_at"` + ExpiresAt string `json:"expires_at"` + LastAccessedAt *string `json:"last_accessed_at,omitempty"` + Authenticators []Authenticator `json:"authenticators,omitempty"` + Link string `json:"link"` + Message string `json:"message,omitempty"` + Title string `json:"title,omitempty"` + NotifyEmail string `json:"notify_email,omitempty"` + Tags Tags `json:"tags,omitempty"` +} + +type ShareLinkCreateResult struct { + ShareLinkObjects []ShareLinkItem `json:"share_link_objects"` +} + +// @summary Create share links +// +// @description Create a share link. Beta API. +// +// @operationId share_post_v1beta_share_link_create +// +// @example +// +// authenticator := share.Authenticator{ +// AuthType: share.ATpassword, +// AuthContext: "my_fav_Pa55word", +// } +// +// link := share.ShareLinkCreateItem{ +// Targets: []string{"pos_3djfmzg2db4c6donarecbyv5begtj2bm"}, +// LinkType: "download", +// Authenticators: []Authenticator{authenticator}, +// } +// +// input := &share.ShareLinkCreateRequest{ +// Links: []share.ShareLinkCreateItem{link}, +// } +// +// res, err := shareClient.ShareLinkCreate(ctx, input) +func (e *share) ShareLinkCreate(ctx context.Context, input *ShareLinkCreateRequest) (*pangea.PangeaResponse[ShareLinkCreateResult], error) { + return request.DoPost(ctx, e.Client, "v1beta/share/link/create", input, &ShareLinkCreateResult{}) +} + +type ShareLinkGetRequest struct { + pangea.BaseRequest + + ID string `json:"id"` +} + +type ShareLinkGetResult struct { + ShareLinkObject ShareLinkItem `json:"share_link_object"` +} + +// @summary Get share link +// +// @description Get a share link. Beta API. +// +// @operationId share_post_v1beta_share_link_get +// +// @example +// +// input := &share.ShareLinkGetRequest{ +// ID: "psl_3djfmzg2db4c6donarecbyv5begtj2bm", +// } +// +// res, err := shareClient.ShareLinkGet(ctx, input) +func (e *share) ShareLinkGet(ctx context.Context, input *ShareLinkGetRequest) (*pangea.PangeaResponse[ShareLinkGetResult], error) { + return request.DoPost(ctx, e.Client, "v1beta/share/link/get", input, &ShareLinkGetResult{}) +} + +type FilterShareLinkList struct { + pangea.FilterBase + id *pangea.FilterMatch[string] + storagePoolID *pangea.FilterMatch[string] + target *pangea.FilterMatch[string] + linkType *pangea.FilterMatch[string] + accessCount *pangea.FilterRange[string] + maxAccessCount *pangea.FilterRange[string] + createdAt *pangea.FilterRange[string] + expiresAt *pangea.FilterRange[string] + lastAccessedAt *pangea.FilterRange[string] + link *pangea.FilterMatch[string] +} + +func NewFilterShareLinkList() *FilterShareLinkList { + filter := make(pangea.Filter) + return &FilterShareLinkList{ + FilterBase: *pangea.NewFilterBase(filter), + id: pangea.NewFilterMatch[string]("id", &filter), + storagePoolID: pangea.NewFilterMatch[string]("storage_pool_id", &filter), + target: pangea.NewFilterMatch[string]("target", &filter), + linkType: pangea.NewFilterMatch[string]("link_type", &filter), + accessCount: pangea.NewFilterRange[string]("access_count", &filter), + maxAccessCount: pangea.NewFilterRange[string]("max_access_count", &filter), + createdAt: pangea.NewFilterRange[string]("created_at", &filter), + expiresAt: pangea.NewFilterRange[string]("expires_at", &filter), + lastAccessedAt: pangea.NewFilterRange[string]("last_accessed_at", &filter), + link: pangea.NewFilterMatch[string]("link", &filter), + } +} + +func (f *FilterShareLinkList) ID() *pangea.FilterMatch[string] { + return f.id +} + +func (f *FilterShareLinkList) StoragePoolID() *pangea.FilterMatch[string] { + return f.storagePoolID +} + +func (f *FilterShareLinkList) Target() *pangea.FilterMatch[string] { + return f.target +} + +func (f *FilterShareLinkList) LinkType() *pangea.FilterMatch[string] { + return f.linkType +} + +func (f *FilterShareLinkList) Link() *pangea.FilterMatch[string] { + return f.link +} + +func (f *FilterShareLinkList) AccessCount() *pangea.FilterRange[string] { + return f.accessCount +} + +func (f *FilterShareLinkList) MaxAccessCount() *pangea.FilterRange[string] { + return f.maxAccessCount +} + +func (f *FilterShareLinkList) CreatedAt() *pangea.FilterRange[string] { + return f.createdAt +} + +func (f *FilterShareLinkList) ExpiresAt() *pangea.FilterRange[string] { + return f.expiresAt +} + +func (f *FilterShareLinkList) LastAccessedAt() *pangea.FilterRange[string] { + return f.lastAccessedAt +} + +type ShareLinkListRequest struct { + pangea.BaseRequest + + Filter pangea.Filter `json:"filter,omitempty"` + Last string `json:"last,omitempty"` + Order *ItemOrder `json:"order,omitempty"` + OrderBy *ShareLinkOrderBy `json:"order_by,omitempty"` + Size int `json:"size,omitempty"` +} + +type ShareLinkListResult struct { + Count int `json:"count"` + ShareLinkObjects []ShareLinkItem `json:"share_link_objects"` +} + +// @summary List share links +// +// @description Look up share links by filter options. Beta API. +// +// @operationId share_post_v1beta_share_link_list +// +// @example +// +// input := &share.ShareLinkListRequest{} +// +// res, err := shareClient.ShareLinkList(ctx, input) +func (e *share) ShareLinkList(ctx context.Context, input *ShareLinkListRequest) (*pangea.PangeaResponse[ShareLinkListResult], error) { + return request.DoPost(ctx, e.Client, "v1beta/share/link/list", input, &ShareLinkListResult{}) +} + +type ShareLinkDeleteRequest struct { + pangea.BaseRequest + + Ids []string `json:"ids"` +} + +type ShareLinkDeleteResult struct { + ShareLinkObjects []ShareLinkItem `json:"share_link_objects"` +} + +// @summary Delete share links +// +// @description Delete share links. Beta API. +// +// @operationId share_post_v1beta_share_link_delete +// +// @example +// +// input := &share.ShareLinkDeleteRequest{ +// Ids: []string{"psl_3djfmzg2db4c6donarecbyv5begtj2bm"}, +// } +// +// res, err := shareClient.ShareLinkDelete(ctx, input) +func (e *share) ShareLinkDelete(ctx context.Context, input *ShareLinkDeleteRequest) (*pangea.PangeaResponse[ShareLinkDeleteResult], error) { + return request.DoPost(ctx, e.Client, "v1beta/share/link/delete", input, &ShareLinkDeleteResult{}) +} + +// @summary Request upload URL +// +// @description Request an upload URL. Beta API. +// +// @operationId share_post_v1beta_put 2 +// +// @example +// +// input := &share.PutRequest{ +// TransferMethod: pangea.TMpostURL, +// CRC32C: "515f7c32", +// SHA256: "c0b56b1a154697f79d27d57a3a2aad4c93849aa2239cd23048fc6f45726271cc", +// Size: 222089, +// Metadata: share.Metadata{ +// "created_by": "jim", +// "priority": "medium", +// }, +// ParentID: "pos_3djfmzg2db4c6donarecbyv5begtj2bm", +// Path: "/", +// Tags: share.Tags{"irs_2023", "personal"}, +// } +// +// res, err := shareClient.RequestUploadURL(ctx, input) +func (e *share) RequestUploadURL(ctx context.Context, input *PutRequest) (*pangea.PangeaResponse[PutResult], error) { + if input.TransferMethod == pangea.TMpostURL && (input.CRC32C == "" || input.SHA256 == "" || input.Size == nil) { + return nil, errors.New("Need to set CRC32C, SHA256 and Size in order to use TransferMethod TMpostURL") + } + + return request.GetUploadURL(ctx, e.Client, "v1beta/put", input, &PutResult{}) +} + +type ShareLinkSendItem struct { + Id string `json:"id"` + Email string `json:"email"` +} + +type ShareLinkSendRequest struct { + pangea.BaseRequest + + Links []ShareLinkSendItem `json:"links"` + SenderEmail string `json:"sender_email"` + SenderName string `json:"sender_name,omitempty"` +} + +type ShareLinkSendResult struct { + ShareLinkObjects []ShareLinkItem `json:"share_link_objects"` +} + +// @summary Send share link(s) +// +// @description Send share link(s) Beta API. +// +// @operationId share_post_v1beta_share_link_send +// +// @example +// +// res, err := client.ShareLinkSend(ctx, &share.ShareLinkSendRequest{ +// Links: []share.ShareLinkSendItem{ +// share.ShareLinkSendItem{ +// Id: link.ID, +// Email: "user@email.com", +// }, +// }, +// SenderEmail: "sender@email.com", +// SenderName: "Sender Name", +// }) +func (e *share) ShareLinkSend(ctx context.Context, input *ShareLinkSendRequest) (*pangea.PangeaResponse[ShareLinkSendResult], error) { + return request.DoPost(ctx, e.Client, "v1beta/share/link/send", input, &ShareLinkSendResult{}) +} + +type FileFormat string + +const ( + FF3g2 FileFormat = "3G2" + FF3gp = "3GP" + FF3mf = "3MF" + FF7z = "7Z" + FFa = "A" + FFaac = "AAC" + FFaccdb = "ACCDB" + FFaiff = "AIFF" + FFamf = "AMF" + FFamr = "AMR" + FFape = "APE" + FFasf = "ASF" + FFatom = "ATOM" + FFau = "AU" + FFavi = "AVI" + FFavif = "AVIF" + FFbin = "BIN" + FFbmp = "BMP" + FFbpg = "BPG" + FFbz2 = "BZ2" + FFcab = "CAB" + FFclass = "CLASS" + FFcpio = "CPIO" + FFcrx = "CRX" + FFcsv = "CSV" + FFdae = "DAE" + FFdbf = "DBF" + FFdcm = "DCM" + FFdeb = "DEB" + FFdjvu = "DJVU" + FFdll = "DLL" + FFdoc = "DOC" + FFdocx = "DOCX" + FFdwg = "DWG" + FFeot = "EOT" + FFepub = "EPUB" + FFexe = "EXE" + FFfdf = "FDF" + FFfits = "FITS" + FFflac = "FLAC" + FFflv = "FLV" + FFgbr = "GBR" + FFgeojson = "GEOJSON" + FFgif = "GIF" + FFglb = "GLB" + FFgml = "GML" + FFgpx = "GPX" + FFgz = "GZ" + FFhar = "HAR" + FFhdr = "HDR" + FFheic = "HEIC" + FFheif = "HEIF" + FFhtml = "HTML" + FFicns = "ICNS" + FFico = "ICO" + FFics = "ICS" + FFiso = "ISO" + FFjar = "JAR" + FFjp2 = "JP2" + FFjpf = "JPF" + FFjpg = "JPG" + FFjpm = "JPM" + FFjs = "JS" + FFjson = "JSON" + FFjxl = "JXL" + FFjxr = "JXR" + FFkml = "KML" + FFlit = "LIT" + FFlnk = "LNK" + FFlua = "LUA" + FFlz = "LZ" + FFm3u = "M3U" + FFm4a = "M4A" + FFmacho = "MACHO" + FFmdb = "MDB" + FFmidi = "MIDI" + FFmkv = "MKV" + FFmobi = "MOBI" + FFmov = "MOV" + FFmp3 = "MP3" + FFmp4 = "MP4" + FFmpc = "MPC" + FFmpeg = "MPEG" + FFmqv = "MQV" + FFmrc = "MRC" + FFmsg = "MSG" + FFmsi = "MSI" + FFndjson = "NDJSON" + FFnes = "NES" + FFodc = "ODC" + FFodf = "ODF" + FFodg = "ODG" + FFodp = "ODP" + FFods = "ODS" + FFodt = "ODT" + FFoga = "OGA" + FFogv = "OGV" + FFotf = "OTF" + FFotg = "OTG" + FFotp = "OTP" + FFots = "OTS" + FFott = "OTT" + FFowl = "OWL" + FFp7s = "P7S" + FFpat = "PAT" + FFpdf = "PDF" + FFphp = "PHP" + FFpl = "PL" + FFpng = "PNG" + FFppt = "PPT" + FFpptx = "PPTX" + FFps = "PS" + FFpsd = "PSD" + FFpub = "PUB" + FFpy = "PY" + FFqcp = "QCP" + FFrar = "RAR" + FFrmvb = "RMVB" + FFrpm = "RPM" + FFrss = "RSS" + FFrtf = "RTF" + FFshp = "SHP" + FFshx = "SHX" + FFso = "SO" + FFsqlite = "SQLITE" + FFsrt = "SRT" + FFsvg = "SVG" + FFswf = "SWF" + FFsxc = "SXC" + FFtar = "TAR" + FFtcl = "TCL" + FFtcx = "TCX" + FFtiff = "TIFF" + FFtorrent = "TORRENT" + FFtsv = "TSV" + FFttc = "TTC" + FFttf = "TTF" + FFtxt = "TXT" + FFvcf = "VCF" + FFvoc = "VOC" + FFvtt = "VTT" + FFwarc = "WARC" + FFwasm = "WASM" + FFwav = "WAV" + FFwebm = "WEBM" + FFwebp = "WEBP" + FFwoff = "WOFF" + FFwoff2 = "WOFF2" + FFx3d = "X3D" + FFxar = "XAR" + FFxcf = "XCF" + FFxfdf = "XFDF" + FFxlf = "XLF" + FFxls = "XLS" + FFxlsx = "XLSX" + FFxml = "XML" + FFxpm = "XPM" + FFxz = "XZ" + FFzip = "ZIP" + FFzst = "ZST" +) diff --git a/pangea-sdk/v3/service/share/integration_test.go b/pangea-sdk/v3/service/share/integration_test.go new file mode 100644 index 00000000..41eb8d0a --- /dev/null +++ b/pangea-sdk/v3/service/share/integration_test.go @@ -0,0 +1,631 @@ +//go:build integration + +package share_test + +import ( + "context" + "fmt" + "os" + "reflect" + "testing" + "time" + + "github.com/pangeacyber/pangea-go/pangea-sdk/v3/internal/pangeatesting" + "github.com/pangeacyber/pangea-go/pangea-sdk/v3/pangea" + "github.com/pangeacyber/pangea-go/pangea-sdk/v3/service/share" + "github.com/stretchr/testify/assert" +) + +var testingEnvironment = pangeatesting.LoadTestEnvironment("share", pangeatesting.Live) +var PDF_FILEPATH = "./testdata/testfile.pdf" +var ZERO_BYTES_FILEPATH = "./testdata/zerobytes.txt" +var timeNow = time.Now() +var TIME = timeNow.Format("20060102_150405") +var FOLDER_DELETE = "/sdk_tests/delete/" + TIME +var FOLDER_FILES = "/sdk_tests/files/" + TIME +var METADATA = map[string]string{"field1": "value1", "field2": "value2"} +var ADD_METADATA = map[string]string{"field3": "value3"} +var TAGS = []string{"tag1", "tag2"} +var ADD_TAGS = []string{"tag3"} + +func shareIntegrationCfg(t *testing.T) *pangea.Config { + t.Helper() + return pangeatesting.IntegrationConfig(t, testingEnvironment) +} + +func Test_Integration_Folder(t *testing.T) { + ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelFn() + + cfg := shareIntegrationCfg(t) + client := share.New(cfg) + + input := &share.FolderCreateRequest{ + Path: FOLDER_DELETE, + } + + out, err := client.FolderCreate(ctx, input) + if err != nil { + fmt.Println(reflect.TypeOf(err)) + t.Fatalf("unexpected error: %v", err) + } + + assert.NotNil(t, out.Result) + assert.NotEmpty(t, out.Result.Object.ID) + assert.NotEmpty(t, out.Result.Object.Name) + assert.NotEmpty(t, out.Result.Object.CreatedAt) + assert.NotEmpty(t, out.Result.Object.UpdatedAt) + assert.Equal(t, out.Result.Object.Type, "folder") + id := out.Result.Object.ID + + input2 := &share.DeleteRequest{ + ID: id, + } + rDel, err := client.Delete(ctx, input2) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + assert.Equal(t, *rDel.Status, "Success") + assert.Equal(t, rDel.Result.Count, 1) +} + +func Test_Integration_PutTransferMethodPostURL(t *testing.T) { + ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + cfg := shareIntegrationCfg(t) + client := share.New(cfg) + + name := TIME + "_file_post_url" + + input := &share.PutRequest{ + Name: name, + TransferRequest: pangea.TransferRequest{ + TransferMethod: pangea.TMpostURL, + }, + } + + file, err := os.Open(PDF_FILEPATH) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + out, err := client.Put(ctx, input, file) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + assert.NotNil(t, out) + assert.NotNil(t, out.Result) + assert.NotEmpty(t, out.Result.Object.ID) + assert.NotEmpty(t, out.Result.Object.Name) + + // Get multipart + getResp, err := client.Get(ctx, + &share.GetRequest{ + ID: out.Result.Object.ID, + TransferMethod: pangea.TMmultipart, + }) + + assert.NoError(t, err) + assert.NotNil(t, getResp) + assert.NotNil(t, getResp.Result) + assert.NotEmpty(t, getResp.Result.Object.ID) + assert.Nil(t, getResp.Result.DestURL) + assert.NotEmpty(t, getResp.AttachedFiles) + assert.Equal(t, len(getResp.AttachedFiles), 1) + getResp.AttachedFiles[0].Save(pangea.AttachedFileSaveInfo{ + Folder: "./download", + }) + + // Get dest-url + getResp, err = client.Get(ctx, + &share.GetRequest{ + ID: out.Result.Object.ID, + TransferMethod: pangea.TMdestURL, + }) + + assert.NoError(t, err) + assert.NotNil(t, getResp) + assert.NotNil(t, getResp.Result) + assert.NotEmpty(t, getResp.Result.Object.ID) + assert.NotNil(t, getResp.Result.DestURL) + assert.Empty(t, getResp.AttachedFiles) +} + +func Test_Integration_PutTransferMethodPostURL_ZeroBytesFile(t *testing.T) { + ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) + defer cancelFn() + + cfg := shareIntegrationCfg(t) + client := share.New(cfg) + + name := TIME + "_file_zero_bytes_post_url" + + input := &share.PutRequest{ + Name: name, + TransferRequest: pangea.TransferRequest{ + TransferMethod: pangea.TMpostURL, + }, + } + + file, err := os.Open(ZERO_BYTES_FILEPATH) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + out, err := client.Put(ctx, input, file) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + assert.NotNil(t, out) + assert.NotNil(t, out.Result) + assert.NotEmpty(t, out.Result.Object.ID) + assert.NotEmpty(t, out.Result.Object.Name) + + // Get multipart + getResp, err := client.Get(ctx, + &share.GetRequest{ + ID: out.Result.Object.ID, + TransferMethod: pangea.TMmultipart, + }) + + assert.NoError(t, err) + assert.NotNil(t, getResp) + assert.NotNil(t, getResp.Result) + assert.NotEmpty(t, getResp.Result.Object.ID) + assert.Nil(t, getResp.Result.DestURL) + assert.NotEmpty(t, getResp.AttachedFiles) + assert.Equal(t, len(getResp.AttachedFiles), 1) + getResp.AttachedFiles[0].Save(pangea.AttachedFileSaveInfo{ + Folder: "./download", + }) + + // Get dest-url + getResp, err = client.Get(ctx, + &share.GetRequest{ + ID: out.Result.Object.ID, + TransferMethod: pangea.TMdestURL, + }) + + assert.NoError(t, err) + assert.NotNil(t, getResp) + assert.NotNil(t, getResp.Result) + assert.NotEmpty(t, getResp.Result.Object.ID) + assert.Nil(t, getResp.Result.DestURL) + assert.Empty(t, getResp.AttachedFiles) +} + +func Test_Integration_PutTransferMethodMultipart(t *testing.T) { + ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelFn() + + cfg := shareIntegrationCfg(t) + client := share.New(cfg) + + name := TIME + "_file_multipart" + + input := &share.PutRequest{ + Name: name, + TransferRequest: pangea.TransferRequest{ + TransferMethod: pangea.TMmultipart, + }, + } + + file, err := os.Open(PDF_FILEPATH) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + out, err := client.Put(ctx, input, file) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + assert.NotNil(t, out) + assert.NotNil(t, out.Result) + assert.NotEmpty(t, out.Result.Object.ID) + assert.NotEmpty(t, out.Result.Object.Name) + + // Get multipart + getResp, err := client.Get(ctx, + &share.GetRequest{ + ID: out.Result.Object.ID, + TransferMethod: pangea.TMmultipart, + }) + + assert.NoError(t, err) + assert.NotNil(t, getResp) + assert.NotNil(t, getResp.Result) + assert.NotEmpty(t, getResp.Result.Object.ID) + assert.Nil(t, getResp.Result.DestURL) + assert.NotEmpty(t, getResp.AttachedFiles) + assert.Equal(t, len(getResp.AttachedFiles), 1) + getResp.AttachedFiles[0].Save(pangea.AttachedFileSaveInfo{ + Folder: "./download", + }) + + // Get dest-url + getResp, err = client.Get(ctx, + &share.GetRequest{ + ID: out.Result.Object.ID, + TransferMethod: pangea.TMdestURL, + }) + + assert.NoError(t, err) + assert.NotNil(t, getResp) + assert.NotNil(t, getResp.Result) + assert.NotEmpty(t, getResp.Result.Object.ID) + assert.NotNil(t, getResp.Result.DestURL) + assert.Empty(t, getResp.AttachedFiles) + +} + +func Test_Integration_PutTransferMethodMultipart_ZeroBytesFile(t *testing.T) { + ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second) + defer cancelFn() + + cfg := shareIntegrationCfg(t) + client := share.New(cfg) + + name := TIME + "_file_zero_bytes_multipart" + + input := &share.PutRequest{ + Name: name, + TransferRequest: pangea.TransferRequest{ + TransferMethod: pangea.TMmultipart, + }, + } + + file, err := os.Open(ZERO_BYTES_FILEPATH) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + out, err := client.Put(ctx, input, file) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + assert.NotNil(t, out) + assert.NotNil(t, out.Result) + assert.NotEmpty(t, out.Result.Object.ID) + assert.NotEmpty(t, out.Result.Object.Name) + + // Get multipart + getResp, err := client.Get(ctx, + &share.GetRequest{ + ID: out.Result.Object.ID, + TransferMethod: pangea.TMmultipart, + }) + + assert.NoError(t, err) + assert.NotNil(t, getResp) + assert.NotNil(t, getResp.Result) + assert.NotEmpty(t, getResp.Result.Object.ID) + assert.Nil(t, getResp.Result.DestURL) + assert.NotEmpty(t, getResp.AttachedFiles) + assert.Equal(t, len(getResp.AttachedFiles), 1) + getResp.AttachedFiles[0].Save(pangea.AttachedFileSaveInfo{ + Folder: "./download", + }) + + // Get dest-url + getResp, err = client.Get(ctx, + &share.GetRequest{ + ID: out.Result.Object.ID, + TransferMethod: pangea.TMdestURL, + }) + + assert.NoError(t, err) + assert.NotNil(t, getResp) + assert.NotNil(t, getResp.Result) + assert.NotEmpty(t, getResp.Result.Object.ID) + assert.Nil(t, getResp.Result.DestURL) + assert.Empty(t, getResp.AttachedFiles) + +} + +func Test_Integration_SplitUpload_Put(t *testing.T) { + ctx, cancelFn := context.WithTimeout(context.Background(), 120*time.Second) + defer cancelFn() + + cfg := pangeatesting.IntegrationConfig(t, testingEnvironment) + client := share.New(cfg) + + name := TIME + "_file_split_put_url" + + input := &share.PutRequest{ + Name: name, + TransferRequest: pangea.TransferRequest{ + TransferMethod: pangea.TMputURL, + }, + } + + file, err := os.Open(PDF_FILEPATH) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + resp, err := client.RequestUploadURL(ctx, input) + if err != nil { + t.Fatalf("unexpected error: %v", err.Error()) + } + + assert.NotNil(t, resp) + assert.NotNil(t, resp.AcceptedResult) + assert.NotEmpty(t, resp.AcceptedResult.PutURL) + assert.Empty(t, resp.AcceptedResult.PostURL) + assert.Empty(t, resp.AcceptedResult.PostFormData) + + url := resp.AcceptedResult.PutURL + + fd := pangea.FileData{ + File: file, + Name: "someName", + } + + uploader := pangea.NewFileUploader() + err = uploader.UploadFile(ctx, url, pangea.TMputURL, fd) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var pr *pangea.PangeaResponse[any] + i := 0 + + for i < 24 { + // Wait until result should be ready + time.Sleep(time.Duration(10 * time.Second)) + + pr, err = client.PollResultByID(ctx, *resp.RequestID, &share.PutResult{}) + if err == nil { + break + } + i++ + } + assert.NoError(t, err) + assert.NotNil(t, pr) + assert.NotNil(t, pr.Result) + + _, ok := (*pr.Result).(*share.PutResult) + assert.True(t, ok) + +} + +func Test_Integration_LifeCycle(t *testing.T) { + ctx, cancelFn := context.WithTimeout(context.Background(), 120*time.Second) + defer cancelFn() + + cfg := pangeatesting.IntegrationConfig(t, testingEnvironment) + client := share.New(cfg) + + // Create a folder + respCreate, err := client.FolderCreate(ctx, &share.FolderCreateRequest{ + Path: FOLDER_FILES, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + assert.NotNil(t, respCreate) + assert.NotNil(t, respCreate.Result) + assert.NotEmpty(t, respCreate.Result.Object.ID) + folderID := respCreate.Result.Object.ID + + // Upload a file with path as unique param + file, err := os.Open(PDF_FILEPATH) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + respPut, err := client.Put(ctx, + &share.PutRequest{ + Path: FOLDER_FILES + "/" + TIME + "_file_multipart_1", + TransferRequest: pangea.TransferRequest{ + TransferMethod: pangea.TMmultipart, + }, + }, + file) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + assert.NotNil(t, respPut) + assert.NotNil(t, respPut.Result) + assert.Equal(t, folderID, respPut.Result.Object.ParentID) + assert.Empty(t, respPut.Result.Object.Metadata) + assert.Empty(t, respPut.Result.Object.Tags) + assert.Empty(t, respPut.Result.Object.MD5) + assert.Empty(t, respPut.Result.Object.SHA512) + assert.NotEmpty(t, respPut.Result.Object.SHA256) + + // Upload a file with parent id and name + file, err = os.Open(PDF_FILEPATH) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + respPut2, err := client.Put(ctx, + &share.PutRequest{ + Name: TIME + "_file_multipart_2", + ParentID: folderID, + TransferRequest: pangea.TransferRequest{ + TransferMethod: pangea.TMmultipart, + }, + Metadata: METADATA, + Tags: TAGS, + }, + file) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + assert.NotNil(t, respPut2) + assert.NotNil(t, respPut2.Result) + assert.Equal(t, folderID, respPut2.Result.Object.ParentID) + assert.Equal(t, share.Metadata(METADATA), respPut2.Result.Object.Metadata) + assert.Equal(t, share.Tags(TAGS), respPut2.Result.Object.Tags) + assert.Empty(t, respPut2.Result.Object.MD5) + assert.Empty(t, respPut2.Result.Object.SHA512) + assert.NotEmpty(t, respPut2.Result.Object.SHA256) + + // Update file with full metadata and tags + respUpdate, err := client.Update(ctx, &share.UpdateRequest{ + ID: respPut.Result.Object.ID, + Metadata: METADATA, + Tags: TAGS, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + assert.NotNil(t, respUpdate) + assert.NotNil(t, respUpdate.Result) + assert.Equal(t, share.Metadata(METADATA), respUpdate.Result.Object.Metadata) + assert.Equal(t, share.Tags(TAGS), respUpdate.Result.Object.Tags) + + // Update file with add metadata and tags + respUpdate2, err := client.Update(ctx, &share.UpdateRequest{ + ID: respPut2.Result.Object.ID, + AddMetadata: ADD_METADATA, + AddTags: ADD_TAGS, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + assert.NotNil(t, respUpdate2) + assert.NotNil(t, respUpdate2.Result) + + // Get archive + respGetArchive, err := client.GetArchive(ctx, &share.GetArchiveRequest{ + Ids: []string{folderID}, + Format: share.AFzip, + TransferMethod: pangea.TMmultipart, + }) + + assert.NoError(t, err) + assert.NotNil(t, respGetArchive) + assert.NotNil(t, respGetArchive.Result) + assert.Nil(t, respGetArchive.Result.DestURL) + assert.Equal(t, len(respGetArchive.AttachedFiles), 1) + + for _, af := range respGetArchive.AttachedFiles { + err := af.Save(pangea.AttachedFileSaveInfo{ + Folder: "./download/archive/", + }) + assert.NoError(t, err) + } + + respGetArchive2, err := client.GetArchive(ctx, &share.GetArchiveRequest{ + Ids: []string{folderID}, + Format: share.AFzip, + TransferMethod: pangea.TMdestURL, + }) + + assert.NoError(t, err) + assert.NotNil(t, respGetArchive2) + assert.NotNil(t, respGetArchive2.Result) + assert.NotNil(t, respGetArchive2.Result.DestURL) + assert.Equal(t, len(respGetArchive2.AttachedFiles), 0) + + // Download file + attachedFile, err := client.DownloadFile(ctx, *respGetArchive2.Result.DestURL) + assert.NoError(t, err) + assert.NotNil(t, attachedFile) + assert.NotEmpty(t, attachedFile.File) + assert.NotEmpty(t, attachedFile.Filename) + assert.NotEmpty(t, attachedFile.ContentType) + + // Create share link + authenticators := []share.Authenticator{share.Authenticator{ + AuthType: share.ATpassword, + AuthContext: "somepassword", + }} + ll := []share.ShareLinkCreateItem{share.ShareLinkCreateItem{ + Targets: []string{folderID}, + LinkType: share.LTeditor, + Authenticators: authenticators, + MaxAccessCount: pangea.Int(3), + Message: "share message", + Title: "share title", + }} + respCreateLink, err := client.ShareLinkCreate(ctx, &share.ShareLinkCreateRequest{ + Links: ll, + }) + + assert.NoError(t, err) + assert.NotNil(t, respCreateLink) + assert.NotNil(t, respCreateLink.Result) + + links := respCreateLink.Result.ShareLinkObjects + assert.Equal(t, len(links), 1) + + link := links[0] + assert.Equal(t, link.AccessCount, 0) + assert.Equal(t, link.MaxAccessCount, 3) + assert.Equal(t, len(link.Authenticators), 1) + assert.Equal(t, string(link.Authenticators[0].AuthType), string(share.ATpassword)) + assert.NotEmpty(t, link.Link) + assert.NotEmpty(t, link.ID) + assert.Equal(t, len(link.Targets), 1) + + // Send share link + respSendLink, err := client.ShareLinkSend(ctx, &share.ShareLinkSendRequest{ + Links: []share.ShareLinkSendItem{ + share.ShareLinkSendItem{ + Id: link.ID, + Email: "user@email.com", + }, + }, + SenderEmail: "sender@email.com", + SenderName: "Sender Name", + }) + + assert.Equal(t, 1, len(respSendLink.Result.ShareLinkObjects)) + + // Get share link + respGetLink, err := client.ShareLinkGet(ctx, &share.ShareLinkGetRequest{ + ID: link.ID, + }) + + assert.NoError(t, err) + assert.NotNil(t, respGetLink) + assert.NotNil(t, respGetLink.Result) + assert.Equal(t, respGetLink.Result.ShareLinkObject, link) + + // List share link + respListLink, err := client.ShareLinkList(ctx, &share.ShareLinkListRequest{}) + + assert.NoError(t, err) + assert.NotNil(t, respListLink) + assert.True(t, respListLink.Result.Count > 0) + assert.True(t, len(respListLink.Result.ShareLinkObjects) > 0) + + // Delete share link + respDeleteLink, err := client.ShareLinkDelete(ctx, &share.ShareLinkDeleteRequest{ + Ids: []string{link.ID}, + }) + + assert.NoError(t, err) + assert.NotNil(t, respDeleteLink) + assert.Equal(t, len(respDeleteLink.Result.ShareLinkObjects), 1) + + // List files in folder + listFilter := share.NewFilterList() + listFilter.Folder().Set(pangea.String(FOLDER_FILES)) + + respList, err := client.List(ctx, &share.ListRequest{ + Filter: listFilter.Filter(), + }) + + assert.NoError(t, err) + assert.NotNil(t, respList) + assert.NotNil(t, respList.Result) + assert.Equal(t, respList.Result.Count, 2) + assert.Equal(t, len(respList.Result.Objects), 2) +} diff --git a/pangea-sdk/v3/service/share/service.go b/pangea-sdk/v3/service/share/service.go new file mode 100644 index 00000000..71783103 --- /dev/null +++ b/pangea-sdk/v3/service/share/service.go @@ -0,0 +1,38 @@ +package share + +import ( + "context" + "os" + + "github.com/pangeacyber/pangea-go/pangea-sdk/v3/pangea" +) + +type Client interface { + FolderCreate(ctx context.Context, input *FolderCreateRequest) (*pangea.PangeaResponse[FolderCreateResult], error) + Delete(ctx context.Context, input *DeleteRequest) (*pangea.PangeaResponse[DeleteResult], error) + Get(ctx context.Context, input *GetRequest) (*pangea.PangeaResponse[GetResult], error) + Put(ctx context.Context, input *PutRequest, file *os.File) (*pangea.PangeaResponse[PutResult], error) + Update(ctx context.Context, input *UpdateRequest) (*pangea.PangeaResponse[UpdateResult], error) + List(ctx context.Context, input *ListRequest) (*pangea.PangeaResponse[ListResult], error) + GetArchive(ctx context.Context, input *GetArchiveRequest) (*pangea.PangeaResponse[GetArchiveResult], error) + ShareLinkCreate(ctx context.Context, input *ShareLinkCreateRequest) (*pangea.PangeaResponse[ShareLinkCreateResult], error) + ShareLinkGet(ctx context.Context, input *ShareLinkGetRequest) (*pangea.PangeaResponse[ShareLinkGetResult], error) + ShareLinkList(ctx context.Context, input *ShareLinkListRequest) (*pangea.PangeaResponse[ShareLinkListResult], error) + ShareLinkDelete(ctx context.Context, input *ShareLinkDeleteRequest) (*pangea.PangeaResponse[ShareLinkDeleteResult], error) + ShareLinkSend(ctx context.Context, input *ShareLinkSendRequest) (*pangea.PangeaResponse[ShareLinkSendResult], error) + RequestUploadURL(ctx context.Context, input *PutRequest) (*pangea.PangeaResponse[PutResult], error) + + // Base service methods + pangea.BaseServicer +} + +type share struct { + pangea.BaseService +} + +func New(cfg *pangea.Config) Client { + cli := &share{ + BaseService: pangea.NewBaseService("share", cfg), + } + return cli +} diff --git a/pangea-sdk/v3/service/share/testdata/testfile.pdf b/pangea-sdk/v3/service/share/testdata/testfile.pdf new file mode 100644 index 00000000..26704774 Binary files /dev/null and b/pangea-sdk/v3/service/share/testdata/testfile.pdf differ diff --git a/pangea-sdk/v3/service/share/testdata/zerobytes.txt b/pangea-sdk/v3/service/share/testdata/zerobytes.txt new file mode 100644 index 00000000..e69de29b