2020import java .io .FileInputStream ;
2121import java .io .IOException ;
2222import java .io .InputStream ;
23- import java .net .URI ;
2423import java .util .ArrayList ;
25- import java .util .Arrays ;
24+ import java .util .Base64 ;
2625import java .util .List ;
2726import org .apache .commons .lang3 .RandomStringUtils ;
27+ import org .apache .http .client .methods .CloseableHttpResponse ;
28+ import org .apache .http .client .methods .HttpGet ;
29+ import org .apache .http .impl .client .CloseableHttpClient ;
30+ import org .apache .http .impl .client .HttpClientBuilder ;
31+ import org .apache .http .util .EntityUtils ;
2832import org .datatransferproject .api .launcher .Monitor ;
2933import org .datatransferproject .datatransfer .backblaze .exception .BackblazeCredentialsException ;
30- import org .datatransferproject . transfer . JobMetadata ;
31- import software . amazon . awssdk . auth . credentials . AwsSessionCredentials ;
32- import software . amazon . awssdk . auth . credentials . StaticCredentialsProvider ;
34+ import org .json . simple . JSONObject ;
35+ import org . json . simple . parser . JSONParser ;
36+ import org . json . simple . parser . ParseException ;
3337import software .amazon .awssdk .awscore .exception .AwsServiceException ;
34- import software .amazon .awssdk .core .client .config .ClientOverrideConfiguration ;
3538import software .amazon .awssdk .core .exception .SdkClientException ;
3639import software .amazon .awssdk .core .sync .RequestBody ;
37- import software .amazon .awssdk .regions .Region ;
3840import software .amazon .awssdk .services .s3 .S3Client ;
3941import software .amazon .awssdk .services .s3 .model .Bucket ;
4042import software .amazon .awssdk .services .s3 .model .BucketAlreadyExistsException ;
5456import software .amazon .awssdk .services .s3 .model .UploadPartRequest ;
5557import software .amazon .awssdk .services .s3 .model .UploadPartResponse ;
5658
59+ /**
60+ * Represents a client for handling data transfer operations with Backblaze B2's S3 compatible API.
61+ * This class is responsible for managing the initialization of connections, uploading files, and
62+ * handling multipart uploads for large files.
63+ *
64+ * <p>The client requires valid Backblaze credentials (keyId and applicationKey) and an instance of
65+ * a pre-configured S3 client factory for communication with the Backblaze API. Additionally, a
66+ * Monitor is used to log diagnostic messages during execution.
67+ *
68+ * <p>The class provides methods to: - Initialize the BackblazeDataTransferClient using user
69+ * credentials. - Upload files either as single uploads or using multipart uploads for larger files.
70+ * - Create or select appropriate buckets for data transfer.
71+ *
72+ * <p>The client implements retry mechanisms and proper error handling for common scenarios like
73+ * network failures or authentication issues.
74+ */
5775public class BackblazeDataTransferClient {
5876 private static final String DATA_TRANSFER_BUCKET_PREFIX_FORMAT_STRING = "%s-data-transfer" ;
5977 private static final int MAX_BUCKET_CREATION_ATTEMPTS = 10 ;
60- private final List <String > BACKBLAZE_REGIONS =
61- Arrays .asList ("us-west-000" , "us-west-001" , "us-west-002" , "eu-central-003" );
6278
6379 private final long sizeThresholdForMultipartUpload ;
6480 private final long partSizeForMultiPartUpload ;
@@ -68,10 +84,10 @@ public class BackblazeDataTransferClient {
6884 private String bucketName ;
6985
7086 public BackblazeDataTransferClient (
71- Monitor monitor ,
72- BackblazeS3ClientFactory backblazeS3ClientFactory ,
73- long sizeThresholdForMultipartUpload ,
74- long partSizeForMultiPartUpload ) {
87+ Monitor monitor ,
88+ BackblazeS3ClientFactory backblazeS3ClientFactory ,
89+ long sizeThresholdForMultipartUpload ,
90+ long partSizeForMultiPartUpload ) {
7591 this .monitor = monitor ;
7692 this .backblazeS3ClientFactory = backblazeS3ClientFactory ;
7793 // Avoid infinite loops
@@ -81,43 +97,25 @@ public BackblazeDataTransferClient(
8197 this .partSizeForMultiPartUpload = partSizeForMultiPartUpload ;
8298 }
8399
84- public void init (String keyId , String applicationKey , String exportService )
100+ public void init (
101+ String keyId , String applicationKey , String exportService , CloseableHttpClient httpClient )
85102 throws BackblazeCredentialsException , IOException {
86103 // Fetch all the available buckets and use that to find which region the user is in
87104 ListBucketsResponse listBucketsResponse = null ;
88- String userRegion = null ;
89-
90- // The Key ID starts with the region identifier number, so reorder the regions such that
91- // the first region is most likely the user's region
92- String regionId = keyId .substring (0 , 3 );
93- BACKBLAZE_REGIONS .sort (
94- (String region1 , String region2 ) -> {
95- if (region1 .endsWith (regionId )) {
96- return -1 ;
97- }
98- return 0 ;
99- });
100105
101106 Throwable s3Exception = null ;
102- for (String region : BACKBLAZE_REGIONS ) {
103- try {
104- s3Client = backblazeS3ClientFactory .createS3Client (keyId , applicationKey , region );
105-
106- listBucketsResponse = s3Client .listBuckets ();
107- userRegion = region ;
108- break ;
109- } catch (S3Exception e ) {
110- s3Exception = e ;
111- if (s3Client != null ) {
112- s3Client .close ();
113- }
114- if (e .statusCode () == 403 ) {
115- monitor .debug (() -> String .format ("User is not in region %s" , region ));
116- }
107+ String userRegion = getAccountRegion (httpClient , keyId , applicationKey );
108+ s3Client = backblazeS3ClientFactory .createS3Client (keyId , applicationKey , userRegion );
109+ try {
110+ listBucketsResponse = s3Client .listBuckets ();
111+ } catch (S3Exception e ) {
112+ s3Exception = e ;
113+ if (s3Client != null ) {
114+ s3Client .close ();
117115 }
118116 }
119117
120- if (listBucketsResponse == null || userRegion == null ) {
118+ if (listBucketsResponse == null ) {
121119 throw new BackblazeCredentialsException (
122120 "User's credentials or permissions are not valid for any regions available" , s3Exception );
123121 }
@@ -140,7 +138,7 @@ public String uploadFile(String fileKey, File file) throws IOException {
140138 () ->
141139 String .format (
142140 "File size is larger than %d bytes, so using multipart upload" ,
143- sizeThresholdForMultipartUpload ));
141+ sizeThresholdForMultipartUpload ));
144142 return uploadFileUsingMultipartUpload (fileKey , file , contentLength );
145143 }
146144
@@ -156,6 +154,43 @@ public String uploadFile(String fileKey, File file) throws IOException {
156154 }
157155 }
158156
157+ private String getAccountRegion (
158+ CloseableHttpClient httpClient , String keyId , String applicationKey )
159+ throws BackblazeCredentialsException {
160+
161+ String auth = keyId + ":" + applicationKey ;
162+ byte [] encodedAuth = Base64 .getEncoder ().encode (auth .getBytes ());
163+ String authHeaderValue = "Basic " + new String (encodedAuth );
164+
165+ HttpGet request = new HttpGet ("https://api.backblazeb2.com/b2api/v2/b2_authorize_account" );
166+ request .addHeader ("Authorization" , authHeaderValue );
167+
168+ try {
169+ CloseableHttpResponse response = httpClient .execute (request );
170+ try (response ) {
171+ int statusCode = response .getStatusLine ().getStatusCode ();
172+
173+ if (statusCode == 200 ) {
174+ String responseBody = EntityUtils .toString (response .getEntity ());
175+ JSONParser parser = new JSONParser ();
176+ JSONObject jsonResponse = (JSONObject ) parser .parse (responseBody );
177+ String s3ApiUrl = (String ) jsonResponse .get ("s3ApiUrl" );
178+ String region = s3ApiUrl .split ("s3." )[1 ].split ("\\ ." )[0 ];
179+ monitor .info (() -> "Region extracted from s3ApiUrl: " + region );
180+ return region ;
181+ } else if (statusCode >= 400 && statusCode < 500 ) {
182+ // Don't retry on client errors (4xx)
183+ throw new BackblazeCredentialsException (
184+ "Failed to retrieve account's region. Status code: " + statusCode , null );
185+ } else {
186+ throw new IOException ("Server returned status code: " + statusCode );
187+ }
188+ }
189+ } catch (IOException | ParseException e ) {
190+ throw new BackblazeCredentialsException ("Failed to retrieve account's region" , e );
191+ }
192+ }
193+
159194 private String uploadFileUsingMultipartUpload (String fileKey , File file , long contentLength )
160195 throws IOException , AwsServiceException , SdkClientException {
161196 List <CompletedPart > completedParts = new ArrayList <>();
@@ -210,9 +245,7 @@ private String getOrCreateBucket(
210245 throws IOException {
211246
212247 String fullPrefix =
213- String .format (
214- DATA_TRANSFER_BUCKET_PREFIX_FORMAT_STRING ,
215- exportService .toLowerCase ());
248+ String .format (DATA_TRANSFER_BUCKET_PREFIX_FORMAT_STRING , exportService .toLowerCase ());
216249 try {
217250 for (Bucket bucket : listBucketsResponse .buckets ()) {
218251 if (bucket .name ().startsWith (fullPrefix )) {
@@ -233,7 +266,7 @@ private String getOrCreateBucket(
233266 .build ();
234267 s3Client .createBucket (createBucketRequest );
235268 return bucketName ;
236- } catch (BucketAlreadyExistsException | BucketAlreadyOwnedByYouException e ) {
269+ } catch (Exception e ) {
237270 monitor .info (() -> "Bucket name already exists" );
238271 }
239272 }
0 commit comments