From ef744d15cb8adfe278edd752722fc2c650438a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=2E=20Andr=C3=A9s=20Tournour?= <111303288+pangea-andrest@users.noreply.github.com> Date: Mon, 18 Mar 2024 18:10:18 -0300 Subject: [PATCH] Add SOS support (#155) Co-authored-by: David Wayman Co-authored-by: Kenan Yildirim --- examples/.examples-ci.yml | 2 + .../file_scan/file_scan_async_crowdstrike.go | 30 +- .../file_scan_async_reversinglabs.go | 30 +- examples/share/folder_create_and_delete.go | 61 ++ examples/share/go.mod | 16 + examples/share/go.sum | 30 + examples/share/item_life_cycle.go | 236 +++++ examples/share/put_split_upload.go | 98 ++ .../share/put_transfer_method_multipart.go | 58 ++ .../share/put_transfer_method_post_url.go | 62 ++ examples/share/testdata/testfile.pdf | Bin 0 -> 10028 bytes pangea-sdk/.sdk-ci.yml | 12 +- pangea-sdk/v3/pangea/file_uploader.go | 33 + pangea-sdk/v3/pangea/pangea.go | 49 +- pangea-sdk/v3/pangea/response.go | 32 +- pangea-sdk/v3/pangea/utils.go | 79 ++ pangea-sdk/v3/service/file_scan/api.go | 2 + pangea-sdk/v3/service/share/api.go | 849 ++++++++++++++++++ .../v3/service/share/integration_test.go | 631 +++++++++++++ pangea-sdk/v3/service/share/service.go | 38 + .../v3/service/share/testdata/testfile.pdf | Bin 0 -> 10028 bytes .../v3/service/share/testdata/zerobytes.txt | 0 22 files changed, 2300 insertions(+), 48 deletions(-) create mode 100644 examples/share/folder_create_and_delete.go create mode 100644 examples/share/go.mod create mode 100644 examples/share/go.sum create mode 100644 examples/share/item_life_cycle.go create mode 100644 examples/share/put_split_upload.go create mode 100644 examples/share/put_transfer_method_multipart.go create mode 100644 examples/share/put_transfer_method_post_url.go create mode 100644 examples/share/testdata/testfile.pdf create mode 100644 pangea-sdk/v3/pangea/file_uploader.go create mode 100644 pangea-sdk/v3/service/share/api.go create mode 100644 pangea-sdk/v3/service/share/integration_test.go create mode 100644 pangea-sdk/v3/service/share/service.go create mode 100644 pangea-sdk/v3/service/share/testdata/testfile.pdf create mode 100644 pangea-sdk/v3/service/share/testdata/zerobytes.txt 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 0000000000000000000000000000000000000000..26704774248aa71cd9fcd48053789418f0176652 GIT binary patch literal 10028 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 OnQjuu>ylW}*G;x)`xE3V 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 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 0000000000000000000000000000000000000000..26704774248aa71cd9fcd48053789418f0176652 GIT binary patch literal 10028 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 OnQjuu>ylW}*G;x)`xE3V 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