From fb08bef715d36a139d8b67f7ef8f3954b6f75791 Mon Sep 17 00:00:00 2001 From: Aryamanz29 Date: Wed, 19 Jun 2024 17:05:44 +0530 Subject: [PATCH 1/5] DVX: 318: Added support for file upload and download using presigned URLs --- atlan/assets/asset.go | 5 +- atlan/assets/client.go | 165 ++++++++++++++++++++++++++++++++++---- atlan/assets/constants.go | 16 +++- atlan/assets/errors.go | 1 - atlan/assets/file.go | 98 ++++++++++++++++++++++ atlan/model/file.go | 29 +++++++ 6 files changed, 293 insertions(+), 21 deletions(-) create mode 100644 atlan/assets/file.go create mode 100644 atlan/model/file.go diff --git a/atlan/assets/asset.go b/atlan/assets/asset.go index 1861b0d..bdec0c6 100644 --- a/atlan/assets/asset.go +++ b/atlan/assets/asset.go @@ -4,12 +4,13 @@ import ( "encoding/json" "errors" "fmt" - "github.com/atlanhq/atlan-go/atlan/model" - "github.com/atlanhq/atlan-go/atlan/model/structs" "hash/fnv" "reflect" "strings" "time" + + "github.com/atlanhq/atlan-go/atlan/model" + "github.com/atlanhq/atlan-go/atlan/model/structs" ) // AtlanObject is an interface that all asset types should implement diff --git a/atlan/assets/client.go b/atlan/assets/client.go index fcf6483..b7f6538 100644 --- a/atlan/assets/client.go +++ b/atlan/assets/client.go @@ -4,12 +4,13 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/atlanhq/atlan-go/atlan/logger" "io" "net/http" "net/url" "os" "strings" + + "github.com/atlanhq/atlan-go/atlan/logger" ) // AtlanClient defines the Atlan API client structure. @@ -187,8 +188,86 @@ func (ac *AtlanClient) DisableLogging() { ac.SetLogger(false, "") } +// Removes authorization from header when using +// s3PresignedUrlFileUpload/Download and returns the removed value. +func (ac *AtlanClient) removeAuthorization() (string, error) { + if headers, ok := ac.requestParams["headers"].(map[string]string); ok { + auth, exists := headers["Authorization"] + if exists { + delete(headers, "Authorization") + return auth, nil + } + return "", nil + } else { + return "", fmt.Errorf("unable to remove the \"Authorization\" key " + + "from AtlanClient.requestParams[\"headers\"]; it is not of type map[string]string") + } +} + +// Restores the authorization to the header when using s3PresignedUrlFileUpload/Download . +func (ac *AtlanClient) restoreAuthorization(auth string) error { + if headers, ok := ac.requestParams["headers"].(map[string]string); ok { + headers["Authorization"] = auth + } else { + return fmt.Errorf("unable to restore the \"Authorization\" key " + + "to AtlanClient.requestParams[\"headers\"]; it is not of type map[string]string") + } + return nil +} + +func (ac *AtlanClient) s3PresignedUrlFileUpload(api *API, uploadFile interface{}) (string, error) { + // Remove authorization and returns the auth value + auth, err := ac.removeAuthorization() + if err != nil { + return "", err + } + + // Ensure the authorization is restored after the API call + defer func() { + if auth != "" { + restoreErr := ac.restoreAuthorization(auth) + if restoreErr != nil { + ac.logger.Errorf("failed to restore authorization: %v", restoreErr) + } + } + }() + + // Call the API with upload file + response, err := ac.CallAPI(api, nil, uploadFile) + if err != nil { + return "", err + } + return string(response), nil +} + +func (ac *AtlanClient) s3PresignedUrlFileDownload(api *API, downloadFile interface{}) (string, error) { + // Remove authorization and returns the auth value + auth, err := ac.removeAuthorization() + if err != nil { + return "", err + } + + // Ensure the authorization is restored after the API call + defer func() { + if auth != "" { + restoreErr := ac.restoreAuthorization(auth) + if restoreErr != nil { + ac.logger.Errorf("failed to restore authorization: %v", restoreErr) + } + } + }() + + // Call the API with download file + response, err := ac.CallAPI(api, nil, downloadFile) + if err != nil { + return "", err + } + return string(response), nil +} + // CallAPI makes a generic API call. func (ac *AtlanClient) CallAPI(api *API, queryParams interface{}, requestObj interface{}) ([]byte, error) { + var downloadFile *os.File params := deepCopy(ac.requestParams) path := ac.host + api.Endpoint.Atlas + api.Path @@ -213,19 +292,35 @@ func (ac *AtlanClient) CallAPI(api *API, queryParams interface{}, requestObj int } if requestObj != nil { - //fmt.Println("Request Object:", requestObj) - requestJSON, err := json.Marshal(requestObj) - logger.Log.Debugf("Request JSON: %s", string(requestJSON)) - if err != nil { - ac.logger.Errorf("error marshaling request object: %v", err) - return nil, fmt.Errorf("error marshaling request object: %v", err) + switch reqObj := requestObj.(type) { + case bytes.Buffer: + // In case of binary data upload + // Make sure to use the presigned URL + // in the API request, i.e: api.Path + path = api.Path + params["data"] = reqObj + params["content_type"] = "application/octet-stream" + case *os.File: + // In case of file download + // Make sure to use the presigned URL + // in the API request, i.e: api.Path + path = api.Path + downloadFile = reqObj + default: + // Otherwise just use `json.Marshal()` + requestJSON, err := json.Marshal(requestObj) + ac.logger.Debugf("Request JSON: %s", string(requestJSON)) + if err != nil { + ac.logger.Errorf("error marshaling request object: %v", err) + return nil, fmt.Errorf("error marshaling request object: %v", err) + } + params["data"] = bytes.NewBuffer(requestJSON) } - params["data"] = bytes.NewBuffer(requestJSON) } ac.logAPICall(api.Method, path) - //logger.Log.Debugf("Params: %v", params) + // Send the request response, err := ac.makeRequest(api.Method, path, params) if err != nil { return nil, handleApiError(response, err) @@ -233,15 +328,29 @@ func (ac *AtlanClient) CallAPI(api *API, queryParams interface{}, requestObj int ac.logHTTPStatus(response) + // Handle API error based on response status code + if response.StatusCode != api.Status { + return nil, handleApiError(response, err) + } + + // Handle file download + if downloadFile != nil { + _, err := io.Copy(downloadFile, response.Body) + if err != nil { + return nil, fmt.Errorf("failed to copy file contents: %v", err) + } + ac.logger.Infof("downloaded file saved to: %s", downloadFile.Name()) + return []byte{}, nil + } + + // Handle JSON response responseJSON, err := io.ReadAll(response.Body) - response.Body.Close() if err != nil { return nil, fmt.Errorf("error reading response body: %v", err) } - if response.StatusCode != api.Status { - return nil, handleApiError(response, err) - } + // Finally, close the request body + response.Body.Close() ac.logResponse(responseJSON) @@ -252,6 +361,8 @@ func (ac *AtlanClient) CallAPI(api *API, queryParams interface{}, requestObj int func (ac *AtlanClient) makeRequest(method, path string, params map[string]interface{}) (*http.Response, error) { var req *http.Request var err error + var contentType string + switch method { case http.MethodGet: req, err = http.NewRequest(method, path, nil) @@ -259,15 +370,25 @@ func (ac *AtlanClient) makeRequest(method, path string, params map[string]interf return nil, ThrowAtlanError(err, CONNECTION_ERROR, nil) } case http.MethodPost, http.MethodPut: - body, ok := params["data"].(io.Reader) + var body io.Reader + data, ok := params["data"] if !ok { - return nil, fmt.Errorf("missing or invalid 'data' parameter for POST/PUT/DELETE request") + return nil, fmt.Errorf("missing 'data' parameter for POST/PUT request") + } + switch v := data.(type) { + case bytes.Buffer: + // Binary data upload + body = &v + case io.Reader: + // JSON payload + body = v + default: + return nil, fmt.Errorf("invalid 'data' parameter type for POST/PUT request") } req, err = http.NewRequest(method, path, body) if err != nil { return nil, ThrowAtlanError(err, CONNECTION_ERROR, nil) } - req.Header.Set("Content-Type", "application/json") case http.MethodDelete: // DELETE requests may not always have a body. var body io.Reader @@ -277,6 +398,7 @@ func (ac *AtlanClient) makeRequest(method, path string, params map[string]interf return nil, fmt.Errorf("invalid 'data' parameter for DELETE request") } } + // Create a new http request req, err = http.NewRequest(method, path, body) if err != nil { return nil, ThrowAtlanError(err, CONNECTION_ERROR, nil) @@ -284,7 +406,6 @@ func (ac *AtlanClient) makeRequest(method, path string, params map[string]interf if body != nil { req.Header.Set("Content-Type", "application/json") } - default: return nil, fmt.Errorf("unsupported HTTP method: %s", method) } @@ -301,6 +422,15 @@ func (ac *AtlanClient) makeRequest(method, path string, params map[string]interf } } + // Set content-type + if ct, ok := params["content_type"].(string); ok { + contentType = ct + } else { + // Default content type + contentType = "application/json" + } + req.Header.Set("Content-Type", contentType) + // Set query parameters queryParams, ok := params["params"].(map[string]string) if ok { @@ -327,6 +457,7 @@ func (ac *AtlanClient) makeRequest(method, path string, params map[string]interf req.URL.RawQuery = query } + // Finally, execute the request return ac.Session.Do(req) } diff --git a/atlan/assets/constants.go b/atlan/assets/constants.go index 2ad5f49..405da6a 100644 --- a/atlan/assets/constants.go +++ b/atlan/assets/constants.go @@ -16,6 +16,9 @@ const ( // Entities API ENTITY_API = "entity/" ENTITY_BULK_API = "entity/bulk/" + + // Files API + FILES_API = "files/" ) // API defines the structure of an API call. @@ -34,7 +37,11 @@ var AtlasEndpoint = Endpoint{ Atlas: "/api/meta/", } -// API calls for Atlas +var HeraclesEndpoint = Endpoint{ + Atlas: "/api/service/", +} + +// API calls to various services (Atlas, Heracles etc) var ( GET_TYPEDEF_BY_NAME = API{ Path: TYPEDEF_BY_NAME, @@ -126,6 +133,13 @@ var ( Status: http.StatusOK, Endpoint: AtlasEndpoint, } + + PRESIGNED_URL = API{ + Path: FILES_API + "presignedUrl", + Method: http.MethodPost, + Status: http.StatusOK, + Endpoint: HeraclesEndpoint, + } ) // Constants for the Atlas search DSL diff --git a/atlan/assets/errors.go b/atlan/assets/errors.go index c46cbed..db2280a 100644 --- a/atlan/assets/errors.go +++ b/atlan/assets/errors.go @@ -707,7 +707,6 @@ func handleApiError(response *http.Response, originalError error) error { default: return ThrowAtlanError(originalError, ERROR_PASSTHROUGH, nil) } - return nil } func ThrowAtlanError(err error, sdkError ErrorCode, suggestion *string, args ...interface{}) error { diff --git a/atlan/assets/file.go b/atlan/assets/file.go new file mode 100644 index 0000000..8362a4c --- /dev/null +++ b/atlan/assets/file.go @@ -0,0 +1,98 @@ +package assets + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + + "github.com/atlanhq/atlan-go/atlan/model" +) + +// A client for operating on Atlan's tenant object storage. +type FileClient struct { + Client *AtlanClient +} + +// NewFileClient creates a new instance of FileClient. +func NewFileClient(client *AtlanClient) *FileClient { + return &FileClient{Client: client} +} + +func handleFileUpload(filePath string, fileBuffer *bytes.Buffer) error { + file, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("error opening file: %v", err) + } + defer file.Close() + + _, err = io.Copy(fileBuffer, file) + if err != nil { + return fmt.Errorf("error copying file: %v", err) + } + return nil +} + +// Generates a presigned URL based on Atlan's tenant object store. +func (client *FileClient) GeneratePresignedURL(request *model.PresignedURLRequest) (string, error) { + rawJSON, err := DefaultAtlanClient.CallAPI(&PRESIGNED_URL, nil, request) + if err != nil { + return "", AtlanError{ + ErrorCode: errorCodes[CONNECTION_ERROR], + Args: []interface{}{"IOException"}, + } + } + // Now unmarshal `rawJSON` to the `PresignedURLResponse` + var response model.PresignedURLResponse + err = json.Unmarshal(rawJSON, &response) + if err != nil { + return "", fmt.Errorf("Error while unmarshaling PresignedURLResponse JSON: %v", err) + } + return response.URL, nil +} + +// Uploads a file to Atlan's object storage. +func (client *FileClient) UploadFile(presignedUrl string, filePath string) (string, error) { + var PRESIGNED_URL_UPLOAD = API{ + Path: presignedUrl, + Method: http.MethodPut, + Status: http.StatusOK, + Endpoint: HeraclesEndpoint, + } + var fileBuffer bytes.Buffer + + err := handleFileUpload(filePath, &fileBuffer) + if err != nil { + return "", err + } + + response, err := DefaultAtlanClient.s3PresignedUrlFileUpload(&PRESIGNED_URL_UPLOAD, fileBuffer) + if err != nil { + return "", err + } + return string(response), nil +} + +// Downloads a file from Atlan's tenant object storage. +func (client *FileClient) DownloadFile(presignedUrl string, filePath string) error { + var PRESIGNED_URL_DOWNLOAD = API{ + Path: presignedUrl, + Method: http.MethodGet, + Status: http.StatusOK, + Endpoint: HeraclesEndpoint, + } + + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to create download file: %v", err) + } + defer file.Close() + + _, err = DefaultAtlanClient.s3PresignedUrlFileDownload(&PRESIGNED_URL_DOWNLOAD, file) + if err != nil { + return err + } + return nil +} diff --git a/atlan/model/file.go b/atlan/model/file.go new file mode 100644 index 0000000..843715c --- /dev/null +++ b/atlan/model/file.go @@ -0,0 +1,29 @@ +package model + +// Method represents the HTTP methods for the presigned URL request. +type Method string + +const ( + GET Method = "GET" + PUT Method = "PUT" +) + +// PresignedURLRequest represents a request to generate a presigned URL. +type PresignedURLRequest struct { + Key string `json:"key"` + Expiry string `json:"expiry"` + Method Method `json:"method"` +} + +type PresignedURLResponse struct { + URL string `json:"url"` +} + +// CloudStorageIdentifier represents cloud storage identifiers. +type CloudStorageIdentifier string + +const ( + S3 CloudStorageIdentifier = "amazonaws.com" + GCS CloudStorageIdentifier = "storage.googleapis.com" + AzureBlob CloudStorageIdentifier = "blob.core.windows.net" +) From 275a7145c90bb5191008a52acde09403a2273dd4 Mon Sep 17 00:00:00 2001 From: Aryamanz29 Date: Sun, 23 Jun 2024 20:23:55 +0530 Subject: [PATCH 2/5] Added integration tests --- atlan/assets/client.go | 20 ++--- atlan/assets/errors.go | 7 ++ atlan/assets/file.go | 26 +++--- atlan/assets/file_client_test.go | 126 ++++++++++++++++++++++++++++++ atlan/assets/test_data/go-sdk.png | Bin 0 -> 95877 bytes atlan/assets/test_data/go-sdk.txt | 1 + 6 files changed, 161 insertions(+), 19 deletions(-) create mode 100644 atlan/assets/file_client_test.go create mode 100644 atlan/assets/test_data/go-sdk.png create mode 100644 atlan/assets/test_data/go-sdk.txt diff --git a/atlan/assets/client.go b/atlan/assets/client.go index b7f6538..2b7c8d8 100644 --- a/atlan/assets/client.go +++ b/atlan/assets/client.go @@ -215,11 +215,11 @@ func (ac *AtlanClient) restoreAuthorization(auth string) error { return nil } -func (ac *AtlanClient) s3PresignedUrlFileUpload(api *API, uploadFile interface{}) (string, error) { +func (ac *AtlanClient) s3PresignedUrlFileUpload(api *API, uploadFile interface{}) error { // Remove authorization and returns the auth value auth, err := ac.removeAuthorization() if err != nil { - return "", err + return err } // Ensure the authorization is restored after the API call @@ -233,18 +233,18 @@ func (ac *AtlanClient) s3PresignedUrlFileUpload(api *API, uploadFile interface{} }() // Call the API with upload file - response, err := ac.CallAPI(api, nil, uploadFile) + _, err = ac.CallAPI(api, nil, uploadFile) if err != nil { - return "", err + return err } - return string(response), nil + return nil } -func (ac *AtlanClient) s3PresignedUrlFileDownload(api *API, downloadFile interface{}) (string, error) { +func (ac *AtlanClient) s3PresignedUrlFileDownload(api *API, downloadFile interface{}) error { // Remove authorization and returns the auth value auth, err := ac.removeAuthorization() if err != nil { - return "", err + return err } // Ensure the authorization is restored after the API call @@ -258,11 +258,11 @@ func (ac *AtlanClient) s3PresignedUrlFileDownload(api *API, downloadFile interfa }() // Call the API with download file - response, err := ac.CallAPI(api, nil, downloadFile) + _, err = ac.CallAPI(api, nil, downloadFile) if err != nil { - return "", err + return err } - return string(response), nil + return nil } // CallAPI makes a generic API call. diff --git a/atlan/assets/errors.go b/atlan/assets/errors.go index db2280a..f252c3e 100644 --- a/atlan/assets/errors.go +++ b/atlan/assets/errors.go @@ -87,6 +87,7 @@ const ( MISSING_CREDENTIALS FULL_UPDATE_ONLY CATEGORIES_CANNOT_BE_ARCHIVED + UNSUPPORTED_PRESIGNED_URL AUTHENTICATION_PASSTHROUGH NO_API_TOKEN EMPTY_API_TOKEN @@ -401,6 +402,12 @@ var errorCodes = map[ErrorCode]ErrorInfo{ ErrorMessage: "Categories cannot be archived (soft-deleted): %s.", UserAction: "Please use the purge operation if you wish to remove a category.", }, + UNSUPPORTED_PRESIGNED_URL: { + HTTPErrorCode: 400, + ErrorID: "ATLAN-GO-400-043", + ErrorMessage: "Provided presigned URL's cloud provider storage is currently not supported for file uploads.", + UserAction: "Please raise a feature request on the GO SDK GitHub to add support for it.", + }, AUTHENTICATION_PASSTHROUGH: { HTTPErrorCode: 401, ErrorID: "ATLAN-GO-401-000", diff --git a/atlan/assets/file.go b/atlan/assets/file.go index 8362a4c..90253dc 100644 --- a/atlan/assets/file.go +++ b/atlan/assets/file.go @@ -7,18 +7,19 @@ import ( "io" "net/http" "os" + "strings" "github.com/atlanhq/atlan-go/atlan/model" ) // A client for operating on Atlan's tenant object storage. type FileClient struct { - Client *AtlanClient + *AtlanClient } // NewFileClient creates a new instance of FileClient. func NewFileClient(client *AtlanClient) *FileClient { - return &FileClient{Client: client} + return &FileClient{client} } func handleFileUpload(filePath string, fileBuffer *bytes.Buffer) error { @@ -37,7 +38,7 @@ func handleFileUpload(filePath string, fileBuffer *bytes.Buffer) error { // Generates a presigned URL based on Atlan's tenant object store. func (client *FileClient) GeneratePresignedURL(request *model.PresignedURLRequest) (string, error) { - rawJSON, err := DefaultAtlanClient.CallAPI(&PRESIGNED_URL, nil, request) + rawJSON, err := client.CallAPI(&PRESIGNED_URL, nil, request) if err != nil { return "", AtlanError{ ErrorCode: errorCodes[CONNECTION_ERROR], @@ -54,7 +55,7 @@ func (client *FileClient) GeneratePresignedURL(request *model.PresignedURLReques } // Uploads a file to Atlan's object storage. -func (client *FileClient) UploadFile(presignedUrl string, filePath string) (string, error) { +func (client *FileClient) UploadFile(presignedUrl string, filePath string) error { var PRESIGNED_URL_UPLOAD = API{ Path: presignedUrl, Method: http.MethodPut, @@ -65,14 +66,21 @@ func (client *FileClient) UploadFile(presignedUrl string, filePath string) (stri err := handleFileUpload(filePath, &fileBuffer) if err != nil { - return "", err + return err + } + + // Currently supported upload methods for different cloud storage providers + switch { + case strings.Contains(presignedUrl, string(model.S3)): + err = client.s3PresignedUrlFileUpload(&PRESIGNED_URL_UPLOAD, fileBuffer) + default: + return InvalidRequestError{AtlanError{ErrorCode: errorCodes[UNSUPPORTED_PRESIGNED_URL]}} } - response, err := DefaultAtlanClient.s3PresignedUrlFileUpload(&PRESIGNED_URL_UPLOAD, fileBuffer) if err != nil { - return "", err + return err } - return string(response), nil + return nil } // Downloads a file from Atlan's tenant object storage. @@ -90,7 +98,7 @@ func (client *FileClient) DownloadFile(presignedUrl string, filePath string) err } defer file.Close() - _, err = DefaultAtlanClient.s3PresignedUrlFileDownload(&PRESIGNED_URL_DOWNLOAD, file) + err = client.s3PresignedUrlFileDownload(&PRESIGNED_URL_DOWNLOAD, file) if err != nil { return err } diff --git a/atlan/assets/file_client_test.go b/atlan/assets/file_client_test.go new file mode 100644 index 0000000..04875f4 --- /dev/null +++ b/atlan/assets/file_client_test.go @@ -0,0 +1,126 @@ +package assets + +import ( + "fmt" + "image" + _ "image/png" + "os" + "testing" + + "github.com/atlanhq/atlan-go/atlan/model" + "github.com/stretchr/testify/assert" +) + +const TestDataDirectoy = "test_data" + +const UrlExpiry = "10s" +const ImageFileName = "go-sdk.png" +const TextFileName = "go-sdk.txt" +const TextDownloadFileName = "go-sdk-download.txt" +const ImageDownloadFileName = "go-sdk-download.png" +const TenantS3BucketDirectory = "presigned-url-sdk-integration-tests" +const ExpectedTextContent = "test data 12345.\n" + +var ImageS3UploadFilePath = fmt.Sprintf("%s/%s", TenantS3BucketDirectory, ImageFileName) +var TextS3UploadFilePath = fmt.Sprintf("%s/%s", TenantS3BucketDirectory, TextFileName) +var UnsupportedURL = "https://unsupported.storage.com/upload" + +func TestIntegrationFile(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ctx := NewContext() + fileClient := NewFileClient(ctx) + + testCases := []struct { + fileName string + downloadName string + s3UploadPath string + expectedFormat string // Expected file format, e.g: "png" or "txt" + }{ + {ImageFileName, ImageDownloadFileName, ImageS3UploadFilePath, "png"}, + {TextFileName, TextDownloadFileName, TextS3UploadFilePath, "txt"}, + } + + for _, tc := range testCases { + // Test S3 upload + requestPUT := model.PresignedURLRequest{ + Key: tc.s3UploadPath, + Expiry: UrlExpiry, + Method: model.PUT, + } + presignedURL := testGeneratePresignedURL(t, fileClient, requestPUT) + + fileToUpload := fmt.Sprintf("%s/%s", TestDataDirectoy, tc.fileName) + testUploadFile(t, fileClient, presignedURL, fileToUpload) + + // Test S3 download + requestGET := model.PresignedURLRequest{ + Key: tc.s3UploadPath, + Expiry: UrlExpiry, + Method: model.GET, + } + presignedURL = testGeneratePresignedURL(t, fileClient, requestGET) + fileDownloadPath := fmt.Sprintf("%s/%s", TestDataDirectoy, tc.downloadName) + testDownloadFile(t, fileClient, presignedURL, fileDownloadPath, tc.expectedFormat) + } + + // Test unsupported URL upload + testUploadUnsupportedURL(t, fileClient, UnsupportedURL, fmt.Sprintf("%s/%s", TestDataDirectoy, TextFileName)) +} + +func testGeneratePresignedURL(t *testing.T, client *FileClient, request model.PresignedURLRequest) string { + url, err := client.GeneratePresignedURL(&request) + if err != nil { + t.Errorf("Error: %v", err) + } + assert.NotNil(t, url, "Generated presigned url cannot be nil") + return url +} + +func testUploadUnsupportedURL(t *testing.T, client *FileClient, presignedURL string, filePath string) { + err := client.UploadFile(presignedURL, filePath) + assert.Error(t, err, "Expected an error for unsupported presigned URL") + assert.Contains(t, err.Error(), "Provided presigned URL's cloud provider storage "+ + "is currently not supported for file uploads.", "Error message should indicate unsupported URL") +} + +func testUploadFile(t *testing.T, client *FileClient, presignedURL string, filePath string) { + err := client.UploadFile(presignedURL, filePath) + if err != nil { + t.Errorf("Encountered an error while uploading: %v", err) + } +} + +func testDownloadFile(t *testing.T, client *FileClient, presignedURL string, filePath string, expectedFormat string) { + err := client.DownloadFile(presignedURL, filePath) + if err != nil { + t.Errorf("Encountered an error while downloading: %v", err) + } + + // Check if the file exists + _, err = os.Stat(filePath) + assert.NoError(t, err, "The file does not exist") + + if expectedFormat == "png" { + // Open the file + file, err := os.Open(filePath) + assert.NoError(t, err, "Failed to open the file") + defer file.Close() + + // Check if it is a PNG image + _, format, err := image.DecodeConfig(file) + assert.NoError(t, err, "Failed to decode the image") + assert.Equal(t, "png", format, "The file is not a PNG image") + } else if expectedFormat == "txt" { + // Read and check the file content + fileContent, err := os.ReadFile(filePath) + assert.NoError(t, err, "Failed to read the text file") + assert.Equal(t, ExpectedTextContent, string(fileContent), "The file content does not match the expected content") + } + + // Remove the file + err = os.Remove(filePath) + assert.NoError(t, err, "Failed to remove the file") +} diff --git a/atlan/assets/test_data/go-sdk.png b/atlan/assets/test_data/go-sdk.png new file mode 100644 index 0000000000000000000000000000000000000000..cfa3e36ded83f68c57cf810644ba82a2a5ef19e4 GIT binary patch literal 95877 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 Date: Thu, 27 Jun 2024 23:00:44 +0530 Subject: [PATCH 3/5] changes: Added progress bar to file upload/download operations --- atlan/assets/client.go | 123 +++++++++++++++++++++++++++++++---------- atlan/assets/file.go | 29 ++++------ go.mod | 19 +++---- go.sum | 43 +++++++------- 4 files changed, 135 insertions(+), 79 deletions(-) diff --git a/atlan/assets/client.go b/atlan/assets/client.go index 2b7c8d8..a0d31f1 100644 --- a/atlan/assets/client.go +++ b/atlan/assets/client.go @@ -11,6 +11,8 @@ import ( "strings" "github.com/atlanhq/atlan-go/atlan/logger" + "github.com/k0kubun/go-ansi" + "github.com/schollz/progressbar/v3" ) // AtlanClient defines the Atlan API client structure. @@ -215,7 +217,26 @@ func (ac *AtlanClient) restoreAuthorization(auth string) error { return nil } -func (ac *AtlanClient) s3PresignedUrlFileUpload(api *API, uploadFile interface{}) error { +// Initialize the file progress bar using default configuration settings +func initFileProgressBar(fileSize int64, description string) *progressbar.ProgressBar { + bar := progressbar.NewOptions(int(fileSize), + progressbar.OptionSetWidth(50), + progressbar.OptionShowBytes(true), + progressbar.OptionSetPredictTime(false), + progressbar.OptionEnableColorCodes(true), + progressbar.OptionSetDescription(description), + progressbar.OptionSetWriter(ansi.NewAnsiStdout()), + progressbar.OptionSetTheme(progressbar.Theme{ + Saucer: "[green]=[reset]", + SaucerHead: "[green]>[reset]", + SaucerPadding: " ", + BarStart: "[", + BarEnd: "]", + })) + return bar +} + +func (ac *AtlanClient) s3PresignedUrlFileUpload(api *API, uploadFile *os.File, uploadFileSize int64) error { // Remove authorization and returns the auth value auth, err := ac.removeAuthorization() if err != nil { @@ -232,15 +253,22 @@ func (ac *AtlanClient) s3PresignedUrlFileUpload(api *API, uploadFile interface{} } }() - // Call the API with upload file - _, err = ac.CallAPI(api, nil, uploadFile) + // Call the API with upload file options + uploadProgressBarDescription := "Uploading file to the object store:" + uploadProgressBar := initFileProgressBar(uploadFileSize, uploadProgressBarDescription) + options := map[string]interface{}{ + "use_presigned_url": true, + "file_size": uploadFileSize, + "progress_bar": uploadProgressBar, + } + _, err = ac.CallAPI(api, nil, uploadFile, options) if err != nil { return err } return nil } -func (ac *AtlanClient) s3PresignedUrlFileDownload(api *API, downloadFile interface{}) error { +func (ac *AtlanClient) s3PresignedUrlFileDownload(api *API, downloadFilePath string) error { // Remove authorization and returns the auth value auth, err := ac.removeAuthorization() if err != nil { @@ -257,8 +285,16 @@ func (ac *AtlanClient) s3PresignedUrlFileDownload(api *API, downloadFile interfa } }() - // Call the API with download file - _, err = ac.CallAPI(api, nil, downloadFile) + // Call the API with download file options + downloadProgressBarDescription := "Downloading file from the object store:" + downloadProgressBar := initFileProgressBar(0, downloadProgressBarDescription) + options := map[string]interface{}{ + "use_presigned_url": true, + "save_file": true, + "file_path": downloadFilePath, + "progress_bar": downloadProgressBar, + } + _, err = ac.CallAPI(api, nil, nil, options) if err != nil { return err } @@ -266,8 +302,10 @@ func (ac *AtlanClient) s3PresignedUrlFileDownload(api *API, downloadFile interfa } // CallAPI makes a generic API call. -func (ac *AtlanClient) CallAPI(api *API, queryParams interface{}, requestObj interface{}) ([]byte, error) { - var downloadFile *os.File +func (ac *AtlanClient) CallAPI(api *API, queryParams interface{}, requestObj interface{}, options ...interface{}) ([]byte, error) { + var saveFile bool + var filePath string + var fileProgressBar *progressbar.ProgressBar params := deepCopy(ac.requestParams) path := ac.host + api.Endpoint.Atlas + api.Path @@ -291,21 +329,36 @@ func (ac *AtlanClient) CallAPI(api *API, queryParams interface{}, requestObj int path += "?" + query.Encode() } + // Check for extra any API call options + if len(options) > 0 { + if optMap, ok := options[0].(map[string]interface{}); ok { + if _, ok := optMap["save_file"].(bool); ok { + saveFile = ok + } + if path, ok := optMap["file_path"].(string); ok { + filePath = path + } + if fs, ok := optMap["file_size"].(int64); ok { + params["content_length"] = fs + } + if _, ok := optMap["use_presigned_url"].(bool); ok { + path = api.Path + } + if bar, ok := optMap["progress_bar"].(*progressbar.ProgressBar); ok { + fileProgressBar = bar + } + } + } + if requestObj != nil { switch reqObj := requestObj.(type) { - case bytes.Buffer: - // In case of binary data upload - // Make sure to use the presigned URL - // in the API request, i.e: api.Path - path = api.Path - params["data"] = reqObj - params["content_type"] = "application/octet-stream" + // In case of file upload/download case *os.File: - // In case of file download - // Make sure to use the presigned URL - // in the API request, i.e: api.Path - path = api.Path - downloadFile = reqObj + if fileProgressBar != nil { + params["progress_bar"] = fileProgressBar + params["data"] = progressbar.NewReader(reqObj, fileProgressBar) + } + params["content_type"] = "application/octet-stream" default: // Otherwise just use `json.Marshal()` requestJSON, err := json.Marshal(requestObj) @@ -334,12 +387,21 @@ func (ac *AtlanClient) CallAPI(api *API, queryParams interface{}, requestObj int } // Handle file download - if downloadFile != nil { - _, err := io.Copy(downloadFile, response.Body) + if saveFile && filePath != "" { + file, err := os.Create(filePath) + if err != nil { + return nil, fmt.Errorf("failed to create download file: %v", err) + } + defer file.Close() + + // Set the progress bar size based on the response content-length + fileProgressBar.ChangeMax64(response.ContentLength) + _, err = io.Copy(io.MultiWriter(file, fileProgressBar), response.Body) if err != nil { return nil, fmt.Errorf("failed to copy file contents: %v", err) } - ac.logger.Infof("downloaded file saved to: %s", downloadFile.Name()) + + ac.logger.Infof("Successfully downloaded file: %s", file.Name()) return []byte{}, nil } @@ -375,13 +437,13 @@ func (ac *AtlanClient) makeRequest(method, path string, params map[string]interf if !ok { return nil, fmt.Errorf("missing 'data' parameter for POST/PUT request") } - switch v := data.(type) { - case bytes.Buffer: - // Binary data upload - body = &v + switch requestData := data.(type) { + case progressbar.Reader: + // File data upload with progressbar reader + body = &requestData case io.Reader: // JSON payload - body = v + body = requestData default: return nil, fmt.Errorf("invalid 'data' parameter type for POST/PUT request") } @@ -431,6 +493,11 @@ func (ac *AtlanClient) makeRequest(method, path string, params map[string]interf } req.Header.Set("Content-Type", contentType) + // Set content-length + if contentLength, ok := params["content_length"].(int64); ok { + req.ContentLength = contentLength + } + // Set query parameters queryParams, ok := params["params"].(map[string]string) if ok { diff --git a/atlan/assets/file.go b/atlan/assets/file.go index 90253dc..a49366b 100644 --- a/atlan/assets/file.go +++ b/atlan/assets/file.go @@ -1,10 +1,9 @@ package assets import ( - "bytes" "encoding/json" "fmt" - "io" + "io/fs" "net/http" "os" "strings" @@ -22,18 +21,17 @@ func NewFileClient(client *AtlanClient) *FileClient { return &FileClient{client} } -func handleFileUpload(filePath string, fileBuffer *bytes.Buffer) error { +func handleFileUpload(filePath string) (*os.File, fs.FileInfo, error) { file, err := os.Open(filePath) if err != nil { - return fmt.Errorf("error opening file: %v", err) + return nil, nil, fmt.Errorf("error opening file: %v", err) } - defer file.Close() - _, err = io.Copy(fileBuffer, file) + fileInfo, err := file.Stat() if err != nil { - return fmt.Errorf("error copying file: %v", err) + return nil, nil, fmt.Errorf("error while getting file info: %v", err) } - return nil + return file, fileInfo, nil } // Generates a presigned URL based on Atlan's tenant object store. @@ -45,6 +43,7 @@ func (client *FileClient) GeneratePresignedURL(request *model.PresignedURLReques Args: []interface{}{"IOException"}, } } + // Now unmarshal `rawJSON` to the `PresignedURLResponse` var response model.PresignedURLResponse err = json.Unmarshal(rawJSON, &response) @@ -62,17 +61,17 @@ func (client *FileClient) UploadFile(presignedUrl string, filePath string) error Status: http.StatusOK, Endpoint: HeraclesEndpoint, } - var fileBuffer bytes.Buffer - err := handleFileUpload(filePath, &fileBuffer) + file, fileInfo, err := handleFileUpload(filePath) if err != nil { return err } + defer file.Close() // Currently supported upload methods for different cloud storage providers switch { case strings.Contains(presignedUrl, string(model.S3)): - err = client.s3PresignedUrlFileUpload(&PRESIGNED_URL_UPLOAD, fileBuffer) + err = client.s3PresignedUrlFileUpload(&PRESIGNED_URL_UPLOAD, file, fileInfo.Size()) default: return InvalidRequestError{AtlanError{ErrorCode: errorCodes[UNSUPPORTED_PRESIGNED_URL]}} } @@ -92,13 +91,7 @@ func (client *FileClient) DownloadFile(presignedUrl string, filePath string) err Endpoint: HeraclesEndpoint, } - file, err := os.Create(filePath) - if err != nil { - return fmt.Errorf("failed to create download file: %v", err) - } - defer file.Close() - - err = client.s3PresignedUrlFileDownload(&PRESIGNED_URL_DOWNLOAD, file) + err := client.s3PresignedUrlFileDownload(&PRESIGNED_URL_DOWNLOAD, filePath) if err != nil { return err } diff --git a/go.mod b/go.mod index 2bd665a..77e5cb7 100644 --- a/go.mod +++ b/go.mod @@ -3,25 +3,22 @@ module github.com/atlanhq/atlan-go go 1.21 require ( - github.com/go-openapi/strfmt v0.23.0 - github.com/go-openapi/swag v0.23.0 + github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 github.com/matoous/go-nanoid v1.5.0 + github.com/schollz/progressbar/v3 v3.14.4 github.com/stretchr/testify v1.9.0 ) require ( - github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-openapi/errors v0.22.0 // indirect - github.com/google/go-cmp v0.5.9 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/kr/pretty v0.3.1 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/oklog/ulid v1.3.1 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect - go.mongodb.org/mongo-driver v1.14.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/term v0.20.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ee2366e..d1b547f 100644 --- a/go.sum +++ b/go.sum @@ -1,42 +1,41 @@ -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= -github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= -github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= -github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg= +github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matoous/go-nanoid v1.5.0 h1:VRorl6uCngneC4oUQqOYtO3S0H5QKFtKuKycFG3euek= github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +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/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= +github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/schollz/progressbar/v3 v3.14.4 h1:W9ZrDSJk7eqmQhd3uxFNNcTr0QL+xuGNI9dEMrw0r74= +github.com/schollz/progressbar/v3 v3.14.4/go.mod h1:aT3UQ7yGm+2ZjeXPqsjTenwL3ddUiuZ0kfQ/2tHlyNI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= -go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From fd62c2322af1cd0a86719c0e542bd6f920952b21 Mon Sep 17 00:00:00 2001 From: Aryamanz29 Date: Mon, 1 Jul 2024 16:08:43 +0530 Subject: [PATCH 4/5] changes: Improved error handling --- atlan/assets/client.go | 69 +++++++++++++++++++++++++----------------- atlan/assets/errors.go | 35 +++++++++++++++++++++ atlan/assets/file.go | 19 +++++++----- 3 files changed, 88 insertions(+), 35 deletions(-) diff --git a/atlan/assets/client.go b/atlan/assets/client.go index a0d31f1..ea1bae3 100644 --- a/atlan/assets/client.go +++ b/atlan/assets/client.go @@ -201,8 +201,12 @@ func (ac *AtlanClient) removeAuthorization() (string, error) { } return "", nil } else { - return "", fmt.Errorf("unable to remove the \"Authorization\" key " + - "from AtlanClient.requestParams[\"headers\"]; it is not of type map[string]string") + return "", InvalidRequestError{ + AtlanError{ + ErrorCode: errorCodes[UNABLE_TO_PERFORM_OPERATION_ON_AUTHORIZATION], + Args: []interface{}{"remove", "from"}, + }, + } } } @@ -211,8 +215,12 @@ func (ac *AtlanClient) restoreAuthorization(auth string) error { if headers, ok := ac.requestParams["headers"].(map[string]string); ok { headers["Authorization"] = auth } else { - return fmt.Errorf("unable to restore the \"Authorization\" key " + - "to AtlanClient.requestParams[\"headers\"]; it is not of type map[string]string") + return InvalidRequestError{ + AtlanError{ + ErrorCode: errorCodes[UNABLE_TO_PERFORM_OPERATION_ON_AUTHORIZATION], + Args: []interface{}{"restore", "to"}, + }, + } } return nil } @@ -226,9 +234,12 @@ func initFileProgressBar(fileSize int64, description string) *progressbar.Progre progressbar.OptionEnableColorCodes(true), progressbar.OptionSetDescription(description), progressbar.OptionSetWriter(ansi.NewAnsiStdout()), + progressbar.OptionOnCompletion(func() { + fmt.Printf("\n") + }), progressbar.OptionSetTheme(progressbar.Theme{ - Saucer: "[green]=[reset]", - SaucerHead: "[green]>[reset]", + Saucer: "[blue]=[reset]", + SaucerHead: "[blue]>[reset]", SaucerPadding: " ", BarStart: "[", BarEnd: "]", @@ -243,16 +254,6 @@ func (ac *AtlanClient) s3PresignedUrlFileUpload(api *API, uploadFile *os.File, u return err } - // Ensure the authorization is restored after the API call - defer func() { - if auth != "" { - restoreErr := ac.restoreAuthorization(auth) - if restoreErr != nil { - ac.logger.Errorf("failed to restore authorization: %v", restoreErr) - } - } - }() - // Call the API with upload file options uploadProgressBarDescription := "Uploading file to the object store:" uploadProgressBar := initFileProgressBar(uploadFileSize, uploadProgressBarDescription) @@ -265,6 +266,14 @@ func (ac *AtlanClient) s3PresignedUrlFileUpload(api *API, uploadFile *os.File, u if err != nil { return err } + + // Restore authorization after API call + err = ac.restoreAuthorization(auth) + if err != nil { + ac.logger.Errorf("failed to restore authorization: %s", err) + return err + } + return nil } @@ -275,16 +284,6 @@ func (ac *AtlanClient) s3PresignedUrlFileDownload(api *API, downloadFilePath str return err } - // Ensure the authorization is restored after the API call - defer func() { - if auth != "" { - restoreErr := ac.restoreAuthorization(auth) - if restoreErr != nil { - ac.logger.Errorf("failed to restore authorization: %v", restoreErr) - } - } - }() - // Call the API with download file options downloadProgressBarDescription := "Downloading file from the object store:" downloadProgressBar := initFileProgressBar(0, downloadProgressBarDescription) @@ -298,6 +297,14 @@ func (ac *AtlanClient) s3PresignedUrlFileDownload(api *API, downloadFilePath str if err != nil { return err } + + // Restore authorization after API call + err = ac.restoreAuthorization(auth) + if err != nil { + ac.logger.Errorf("failed to restore authorization: %s", err) + return err + } + return nil } @@ -390,7 +397,10 @@ func (ac *AtlanClient) CallAPI(api *API, queryParams interface{}, requestObj int if saveFile && filePath != "" { file, err := os.Create(filePath) if err != nil { - return nil, fmt.Errorf("failed to create download file: %v", err) + return nil, AtlanError{ + ErrorCode: errorCodes[UNABLE_TO_PREPARE_DOWNLOAD_FILE], + Args: []interface{}{err.Error()}, + } } defer file.Close() @@ -398,7 +408,10 @@ func (ac *AtlanClient) CallAPI(api *API, queryParams interface{}, requestObj int fileProgressBar.ChangeMax64(response.ContentLength) _, err = io.Copy(io.MultiWriter(file, fileProgressBar), response.Body) if err != nil { - return nil, fmt.Errorf("failed to copy file contents: %v", err) + return nil, AtlanError{ + ErrorCode: errorCodes[UNABLE_TO_COPY_DOWNLOAD_FILE_CONTENTS], + Args: []interface{}{err.Error()}, + } } ac.logger.Infof("Successfully downloaded file: %s", file.Name()) diff --git a/atlan/assets/errors.go b/atlan/assets/errors.go index f252c3e..9b4a77b 100644 --- a/atlan/assets/errors.go +++ b/atlan/assets/errors.go @@ -88,6 +88,11 @@ const ( FULL_UPDATE_ONLY CATEGORIES_CANNOT_BE_ARCHIVED UNSUPPORTED_PRESIGNED_URL + UNABLE_TO_PREPARE_UPLOAD_FILE + UNABLE_TO_PREPARE_DOWNLOAD_FILE + UNABLE_TO_COPY_DOWNLOAD_FILE_CONTENTS + UNABLE_TO_UNMARSHAL_PRESIGNED_URL_RESPONSE + UNABLE_TO_PERFORM_OPERATION_ON_AUTHORIZATION AUTHENTICATION_PASSTHROUGH NO_API_TOKEN EMPTY_API_TOKEN @@ -408,6 +413,36 @@ var errorCodes = map[ErrorCode]ErrorInfo{ ErrorMessage: "Provided presigned URL's cloud provider storage is currently not supported for file uploads.", UserAction: "Please raise a feature request on the GO SDK GitHub to add support for it.", }, + UNABLE_TO_PREPARE_UPLOAD_FILE: { + HTTPErrorCode: 400, + ErrorID: "ATLAN-GO-400-044", + ErrorMessage: "Unable to prepare the file for upload: %s.", + UserAction: "Please verify that the file exists and ensure it is supported for object store upload.", + }, + UNABLE_TO_PREPARE_DOWNLOAD_FILE: { + HTTPErrorCode: 400, + ErrorID: "ATLAN-GO-400-045", + ErrorMessage: "Unable to create a file for download: %s.", + UserAction: "Check the error message for details and report a bug on the GO SDK GitHub if it persists.", + }, + UNABLE_TO_COPY_DOWNLOAD_FILE_CONTENTS: { + HTTPErrorCode: 400, + ErrorID: "ATLAN-GO-400-046", + ErrorMessage: "Unable to copy download file contents: %s.", + UserAction: "Please verify that the API response body is in the correct format for file download.", + }, + UNABLE_TO_UNMARSHAL_PRESIGNED_URL_RESPONSE: { + HTTPErrorCode: 400, + ErrorID: "ATLAN-GO-400-047", + ErrorMessage: "Unable to unmarshal PresignedURLResponse JSON: %s.", + UserAction: "Check the details of the server's message to correct your request.", + }, + UNABLE_TO_PERFORM_OPERATION_ON_AUTHORIZATION: { + HTTPErrorCode: 400, + ErrorID: "ATLAN-GO-400-048", + ErrorMessage: "Unable to %s the \"Authorization\" key %s AtlanClient request headers.", + UserAction: "Please double-check the type of the \"Authorization\" key; it should be map[string]string.", + }, AUTHENTICATION_PASSTHROUGH: { HTTPErrorCode: 401, ErrorID: "ATLAN-GO-401-000", diff --git a/atlan/assets/file.go b/atlan/assets/file.go index a49366b..5a5e0e7 100644 --- a/atlan/assets/file.go +++ b/atlan/assets/file.go @@ -24,7 +24,7 @@ func NewFileClient(client *AtlanClient) *FileClient { func handleFileUpload(filePath string) (*os.File, fs.FileInfo, error) { file, err := os.Open(filePath) if err != nil { - return nil, nil, fmt.Errorf("error opening file: %v", err) + return nil, nil, fmt.Errorf("error while opening file: %v", err) } fileInfo, err := file.Stat() @@ -38,17 +38,18 @@ func handleFileUpload(filePath string) (*os.File, fs.FileInfo, error) { func (client *FileClient) GeneratePresignedURL(request *model.PresignedURLRequest) (string, error) { rawJSON, err := client.CallAPI(&PRESIGNED_URL, nil, request) if err != nil { - return "", AtlanError{ - ErrorCode: errorCodes[CONNECTION_ERROR], - Args: []interface{}{"IOException"}, - } + return "", err } // Now unmarshal `rawJSON` to the `PresignedURLResponse` var response model.PresignedURLResponse err = json.Unmarshal(rawJSON, &response) if err != nil { - return "", fmt.Errorf("Error while unmarshaling PresignedURLResponse JSON: %v", err) + return "", InvalidRequestError{ + AtlanError{ + ErrorCode: errorCodes[UNABLE_TO_PREPARE_UPLOAD_FILE], + Args: []interface{}{err.Error()}, + }} } return response.URL, nil } @@ -64,7 +65,11 @@ func (client *FileClient) UploadFile(presignedUrl string, filePath string) error file, fileInfo, err := handleFileUpload(filePath) if err != nil { - return err + return InvalidRequestError{ + AtlanError{ + ErrorCode: errorCodes[UNABLE_TO_PREPARE_UPLOAD_FILE], + Args: []interface{}{err.Error()}, + }} } defer file.Close() From 75430c635913273d0e085fc1f492eb44d4805020 Mon Sep 17 00:00:00 2001 From: Aryamanz29 Date: Tue, 9 Jul 2024 22:28:14 +0530 Subject: [PATCH 5/5] change: let the `os.Create()` method handle the user-provided file path for download --- atlan/assets/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atlan/assets/client.go b/atlan/assets/client.go index ea1bae3..f7d785e 100644 --- a/atlan/assets/client.go +++ b/atlan/assets/client.go @@ -394,7 +394,7 @@ func (ac *AtlanClient) CallAPI(api *API, queryParams interface{}, requestObj int } // Handle file download - if saveFile && filePath != "" { + if saveFile { file, err := os.Create(filePath) if err != nil { return nil, AtlanError{