backend
=> [
'AnyEvent'
],
'deferred'
;
use
vars
qw(@CARP_NOT $VERSION)
;
$VERSION
=
'0.02'
;
@CARP_NOT
=
qw(Backblaze::B2::v1::Synchronous)
;
sub
isAsync { 1 }
sub
api {
$_
[0] }
sub
asyncApi {
$_
[0] }
sub
new {
my
(
$class
,
%options
) =
@_
;
$options
{ api_base } //=
$Backblaze::B2::v1::API_BASE
=
$Backblaze::B2::v1::API_BASE
;
croak
"Need an API base"
unless
$options
{ api_base };
bless
\
%options
=>
$class
;
}
sub
log_message {
my
(
$self
) =
shift
;
if
(
$self
->{log_message}) {
goto
&{
$self
->{log_message}};
};
}
sub
read_credentials {
my
(
$self
,
$file
) =
@_
;
if
( !
defined
$file
) {
$file
= File::HomeDir->my_home .
"/credentials.b2"
;
$self
->log_message(0,
"Using default credentials file '$file'"
);
};
$self
->log_message(1,
"Reading credentials from '$file'"
);
open
my
$fh
,
'<'
,
$file
or croak
"Couldn't read credentials from '$file': $!"
;
binmode
$fh
;
local
$/;
my
$json
= <
$fh
>;
my
$cred
= decode_json(
$json
);
$self
->{credentials} =
$cred
;
$cred
};
sub
decode_json_response {
my
(
$self
,
$body
,
$hdr
) =
@_
;
$self
->log_message(1,
sprintf
"HTTP Response status %d"
,
$hdr
->{Status});
my
@result
;
if
( !
$body
) {
$self
->log_message(4,
sprintf
"No response body received"
);
@result
= (0,
"No response body received"
,
$hdr
);
}
else
{
my
$b
=
eval
{ decode_json(
$body
); };
if
(
my
$err
= $@ ) {
$self
->log_message(4,
sprintf
"Error decoding JSON response body: %s"
,
$err
);
@result
= (0,
sprintf
(
"Error decoding JSON response body: %s"
,
$err
),
$hdr
);
}
elsif
(
$hdr
->{Status} =~ /^[45]\d\d$/ ) {
my
$reason
=
$b
->{message} ||
$hdr
->{Reason};
my
$status
=
$b
->{status} ||
$hdr
->{Status};
$self
->log_message(4,
sprintf
"HTTP error status: %s: %s"
,
$status
,
$reason
);
@result
= ( 0,
sprintf
(
sprintf
"HTTP error status: %s: %s"
,
$status
,
$reason
));
}
else
{
@result
= (1,
""
,
$b
);
};
};
@result
}
sub
get_headers {
my
(
$self
) =
@_
;
if
(
my
$token
=
$self
->authorizationToken ) {
return
Authorization
=>
$token
};
return
()
}
sub
accountId {
my
(
$self
) =
@_
;
$self
->{credentials}->{accountId}
}
sub
authorizationToken {
my
(
$self
) =
@_
;
$self
->{credentials}->{authorizationToken}
}
sub
downloadUrl {
my
(
$self
) =
@_
;
$self
->{credentials}->{downloadUrl}
}
sub
apiUrl {
my
(
$self
) =
@_
;
$self
->{credentials}->{apiUrl}
}
sub
request {
my
(
$self
,
%options
) =
@_
;
$options
{ method } ||=
'GET'
;
my
$method
=
delete
$options
{ method };
my
$endpoint
=
delete
$options
{ api_endpoint };
my
$headers
=
delete
$options
{ headers } || {};
$headers
= {
$self
->get_headers,
%$headers
};
my
$body
=
delete
$options
{ _body };
my
$url
;
if
( !
$options
{url} ) {
croak
"Don't know the api_endpoint for the request"
unless
$endpoint
;
$url
= URI->new(
join
(
"/b2api/v1/"
,
$self
->apiUrl,
$endpoint
)
);
}
else
{
$url
=
delete
$options
{ url };
$url
= URI->new(
$url
)
if
( !
ref
$url
);
};
for
my
$k
(
keys
%options
) {
my
$v
=
$options
{
$k
};
$url
->query_param_append(
$k
,
$v
);
};
$self
->log_message(1,
sprintf
"Sending %s request to %s"
,
$method
,
$url
);
my
$res
= deferred;
my
$req
;
$req
= http_request
$method
=>
$url
,
headers
=>
$headers
,
body
=>
$body
,
sub
{
my
(
$data
,
$headers
) =
@_
;
undef
$req
;
$res
->resolve(
$data
,
$headers
);
},
;
$res
->promise
}
sub
json_request {
my
(
$self
,
%options
) =
@_
;
$self
->request(
%options
)->then(
sub
{
my
(
$body
,
$headers
) =
@_
;
my
$d
= deferred;
my
@decoded
=
$self
->decode_json_response(
$body
,
$headers
);
my
$result
=
$d
->promise;
$d
->resolve(
@decoded
);
$result
});
}
sub
authorize_account {
my
(
$self
,
%options
) =
@_
;
$options
{ accountId }
or croak
"Need an accountId"
;
$options
{ applicationKey }
or croak
"Need an applicationKey"
;
my
$auth
= encode_base64(
"$options{accountId}:$options{ applicationKey }"
);
my
$url
=
$self
->{api_base} .
"b2_authorize_account"
;
$self
->json_request(
url
=>
$url
,
headers
=> {
"Authorization"
=>
"Basic $auth"
},
)->then(
sub
{
my
(
$ok
,
$msg
,
$cred
) =
@_
;
if
(
$ok
) {
$self
->log_message(1,
sprintf
"Storing authorization token"
);
$self
->{credentials} =
$cred
;
};
return
(
$ok
,
$msg
,
$cred
);
});
}
sub
create_bucket {
my
(
$self
,
%options
) =
@_
;
croak
"Need a bucket name"
unless
defined
$options
{ bucketName };
$options
{ accountId } ||=
$self
->accountId;
$options
{ bucketType } ||=
'allPrivate'
;
$self
->json_request(
api_endpoint
=>
'b2_create_bucket'
,
accountId
=>
$options
{ accountId },
bucketName
=>
$options
{ bucketName },
bucketType
=>
$options
{ bucketType },
%options
)
}
sub
delete_bucket {
my
(
$self
,
%options
) =
@_
;
croak
"Need a bucketId"
unless
defined
$options
{ bucketId };
$options
{ accountId } ||=
$self
->accountId;
my
$res
= AnyEvent->condvar;
$self
->json_request(
api_endpoint
=>
'b2_delete_bucket'
,
accountId
=>
$options
{ accountId },
bucketId
=>
$options
{ bucketId },
%options
);
}
sub
list_buckets {
my
(
$self
,
%options
) =
@_
;
$options
{ accountId } ||=
$self
->accountId;
$self
->json_request(
api_endpoint
=>
'b2_list_buckets'
,
accountId
=>
$options
{ accountId },
%options
)
}
sub
get_upload_url {
my
(
$self
,
%options
) =
@_
;
croak
"Need a bucketId"
unless
defined
$options
{ bucketId };
$self
->json_request(
api_endpoint
=>
'b2_get_upload_url'
,
%options
)
}
sub
upload_file {
my
(
$self
,
%options
) =
@_
;
croak
"Need an upload handle"
unless
defined
$options
{ handle };
my
$handle
=
delete
$options
{ handle };
croak
"Need a source file name"
unless
defined
$options
{ file };
my
$filename
=
delete
$options
{ file };
my
$target_filename
=
delete
$options
{ target_name };
$target_filename
||=
$filename
;
$target_filename
=~ s!\\!/!g;
$target_filename
= encode(
'UTF-8'
,
$target_filename
);
$target_filename
=~ s!([^\x21-\x7d])!
sprintf
"%%%02x"
,
ord
$1!ge;
my
$mime_type
=
delete
$options
{ mime_type } ||
'b2/x-auto'
;
if
( not
defined
$options
{ content }) {
open
my
$fh
,
'<'
,
$filename
or croak
"Couldn't open '$filename': $!"
;
binmode
$fh
,
':raw'
;
$options
{ content } =
do
{
local
$/; <
$fh
> };
$options
{ mtime } = ((
stat
(
$fh
))[9]) * 1000;
};
my
$payload
=
delete
$options
{ content };
if
( not
$options
{ sha1 }) {
my
$sha1
= Digest::SHA1->new;
$sha1
->add(
$payload
);
$options
{ sha1 } =
$sha1
->hexdigest;
};
my
$digest
=
delete
$options
{ sha1 };
my
$size
=
length
(
$payload
);
my
$mtime
=
delete
$options
{ mtime };
$self
->json_request(
url
=>
$handle
->{uploadUrl},
method
=>
'POST'
,
_body
=>
$payload
,
headers
=> {
'Content-Type'
=>
$mime_type
,
'Content-Length'
=>
$size
,
'X-Bz-Content-Sha1'
=>
$digest
,
'X-Bz-File-Name'
=>
$target_filename
,
'Authorization'
=>
$handle
->{authorizationToken},
},
%options
);
}
sub
list_file_names {
my
(
$self
,
%options
) =
@_
;
croak
"Need a bucket id"
unless
defined
$options
{ bucketId };
$self
->json_request(
api_endpoint
=>
'b2_list_file_names'
,
%options
);
}
sub
list_all_file_names {
my
(
$self
,
%options
) =
@_
;
croak
"Need a bucket id"
unless
defined
$options
{ bucketId };
my
@results
;
my
$handle_response
;
$handle_response
=
sub
{
my
(
$ok
,
$msg
,
$results
) =
@_
;
$self
->log_message(1,
sprintf
"Got filenames starting from '%s' to '%s'"
,
$options
{startFileName} ||
''
,
$results
->{nextFileName} ||
''
);
push
@results
, @{
$results
->{files} };
if
(
$results
->{ endFileName }) {
$options
{ startFileName } =
$results
->{nextFileName};
$self
->log_message(1,
sprintf
"Requesting filenames starting from '%s'"
,
$options
{startFileName} ||
''
);
return
$self
->list_file_names(
%options
)
->then(
$handle_response
);
}
else
{
my
$res
= deferred;
$res
->resolve(1,
""
,
@results
);
$res
->promise
}
};
$self
->log_message(1,
sprintf
"Requesting filenames starting from '%s'"
,
$options
{startFileName} ||
''
);
$self
->list_file_names(
%options
)
->then(
$handle_response
);
}
sub
download_file_by_name {
my
(
$self
,
%options
) =
@_
;
croak
"Need a bucket name"
unless
defined
$options
{ bucketName };
croak
"Need a file name"
unless
defined
$options
{ fileName };
my
$url
=
join
'/'
,
$self
->{credentials}->{downloadUrl},
'file'
,
delete
$options
{ bucketName },
delete
$options
{ fileName }
;
$self
->log_message(1,
sprintf
"Fetching %s"
,
$url
);
$self
->request(
url
=>
$url
,
%options
)->then(
sub
{
my
(
$body
,
$hdr
) =
@_
;
$self
->log_message(2,
sprintf
"Fetching %s, received %d bytes"
,
$url
,
length
$body
);
my
$ok
=
$hdr
->{Status} =~ /^2\d\d/;
return
(
$ok
,
$hdr
->{Reason},
$body
);
})
}
sub
get_download_authorization {
my
(
$self
,
%options
) =
@_
;
croak
"Need a bucket id"
unless
defined
$options
{ bucketId };
croak
"Need a file name prefix"
unless
defined
$options
{ fileNamePrefix };
croak
"Need a duration for the token"
unless
defined
$options
{ validDurationInSeconds };
$self
->json_request(
api_endpoint
=>
'b2_get_download_authorization'
,
%options
);
}
1;