antd 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +143 -9
- data/lib/antd/client.rb +242 -0
- data/lib/antd/errors.rb +133 -4
- data/lib/antd/grpc_client.rb +192 -1
- data/lib/antd/grpc_manifests.rb +128 -0
- data/lib/antd/manifests.rb +300 -0
- data/lib/antd/models.rb +221 -0
- data/lib/antd/v1/data_pb.rb +1 -1
- data/lib/antd/v1/manifests_pb.rb +22 -0
- data/lib/antd/v1/manifests_services_pb.rb +39 -0
- data/lib/antd/v1/pointers_pb.rb +38 -0
- data/lib/antd/v1/pointers_services_pb.rb +59 -0
- data/lib/antd/version.rb +1 -1
- data/lib/antd.rb +2 -0
- metadata +8 -2
data/lib/antd/grpc_client.rb
CHANGED
|
@@ -17,6 +17,8 @@ require_relative "v1/chunks_services_pb"
|
|
|
17
17
|
require_relative "v1/files_services_pb"
|
|
18
18
|
require_relative "v1/upload_services_pb"
|
|
19
19
|
require_relative "v1/wallet_services_pb"
|
|
20
|
+
require_relative "v1/pointers_services_pb"
|
|
21
|
+
require "base64"
|
|
20
22
|
|
|
21
23
|
module Antd
|
|
22
24
|
DEFAULT_GRPC_TARGET = "localhost:50051"
|
|
@@ -47,6 +49,7 @@ module Antd
|
|
|
47
49
|
@file_stub = Antd::V1::FileService::Stub.new(target, :this_channel_is_insecure)
|
|
48
50
|
@upload_stub = Antd::V1::UploadService::Stub.new(target, :this_channel_is_insecure)
|
|
49
51
|
@wallet_stub = Antd::V1::WalletService::Stub.new(target, :this_channel_is_insecure)
|
|
52
|
+
@pointer_stub = Antd::V1::PointerService::Stub.new(target, :this_channel_is_insecure)
|
|
50
53
|
end
|
|
51
54
|
|
|
52
55
|
# --- Health ---
|
|
@@ -468,8 +471,183 @@ module Antd
|
|
|
468
471
|
resp.approved
|
|
469
472
|
end
|
|
470
473
|
|
|
474
|
+
# --- Pointers ---
|
|
475
|
+
#
|
|
476
|
+
# The same methods, models and errors as the REST +Client+ (see there),
|
|
477
|
+
# over PointerService. The pointer errors share gRPC status codes with
|
|
478
|
+
# older ones, so the daemon names them in the +x-antd-error-code+
|
|
479
|
+
# trailer, which +grpc_call+ reads (see +Antd.error_for_grpc_metadata+).
|
|
480
|
+
# Unset optionals arrive as "" or an absent message and read as nil, as
|
|
481
|
+
# REST's nulls do. Every write pays: do not retry +pointer_create+,
|
|
482
|
+
# +pointer_update+, +pointer_transfer+ or +pointer_finalize+
|
|
483
|
+
# automatically.
|
|
484
|
+
|
|
485
|
+
# Mirrors +Client#pointer_keys+.
|
|
486
|
+
# @return [Array<String>]
|
|
487
|
+
def pointer_keys
|
|
488
|
+
resp = grpc_call { @pointer_stub.list_keys(Antd::V1::ListPointerKeysRequest.new) }
|
|
489
|
+
resp.keys.map(&:address)
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# Mirrors +Client#pointer_create+. Pays; do not retry automatically.
|
|
493
|
+
# @return [PointerWriteResult]
|
|
494
|
+
def pointer_create(owner, target, kind: nil)
|
|
495
|
+
req = Antd::V1::PointerWriteRequest.new(owner: owner, target: target, kind: kind.to_s)
|
|
496
|
+
pointer_write_result(grpc_call { @pointer_stub.create(req) })
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Mirrors +Client#pointer_update+. Pays; do not retry automatically.
|
|
500
|
+
# @return [PointerWriteResult]
|
|
501
|
+
def pointer_update(owner, target, kind: nil)
|
|
502
|
+
req = Antd::V1::PointerWriteRequest.new(owner: owner, target: target, kind: kind.to_s)
|
|
503
|
+
pointer_write_result(grpc_call { @pointer_stub.update(req) })
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# Mirrors +Client#pointer_transfer+. Pays; do not retry automatically.
|
|
507
|
+
# @return [PointerTransferResult]
|
|
508
|
+
def pointer_transfer(owner, recipient)
|
|
509
|
+
req = Antd::V1::PointerTransferRequest.new(owner: owner, recipient: recipient)
|
|
510
|
+
resp = grpc_call { @pointer_stub.transfer(req) }
|
|
511
|
+
PointerTransferResult.new(
|
|
512
|
+
address: resp.address,
|
|
513
|
+
state_id: resp.state_id,
|
|
514
|
+
finality: resp.finality.nil? ? nil : pointer_finality_from(resp.finality),
|
|
515
|
+
finality_error: presence(resp.finality_error)
|
|
516
|
+
)
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# Mirrors +Client#pointer_get+.
|
|
520
|
+
# @return [PointerRecord]
|
|
521
|
+
def pointer_get(address)
|
|
522
|
+
resp = grpc_call { @pointer_stub.get(Antd::V1::GetPointerRequest.new(address: address)) }
|
|
523
|
+
PointerRecord.new(
|
|
524
|
+
address: resp.address,
|
|
525
|
+
owner: resp.owner,
|
|
526
|
+
counter: Antd.parse_pointer_counter("counter", resp.counter),
|
|
527
|
+
kind: resp.kind,
|
|
528
|
+
kind_tag: resp.kind_tag,
|
|
529
|
+
target: resp.target,
|
|
530
|
+
state_id: resp.state_id,
|
|
531
|
+
is_final: resp.is_final,
|
|
532
|
+
transferred_to: presence(resp.transferred_to)
|
|
533
|
+
)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Mirrors +Client#pointer_resolve+.
|
|
537
|
+
# @return [PointerTarget]
|
|
538
|
+
def pointer_resolve(address)
|
|
539
|
+
resp = grpc_call { @pointer_stub.resolve(Antd::V1::ResolvePointerRequest.new(address: address)) }
|
|
540
|
+
PointerTarget.new(kind: resp.kind, kind_tag: resp.kind_tag, target: resp.target)
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# Mirrors +Client#pointer_finality+.
|
|
544
|
+
# @return [PointerFinality]
|
|
545
|
+
def pointer_finality(address)
|
|
546
|
+
resp = grpc_call { @pointer_stub.finality(Antd::V1::PointerFinalityRequest.new(address: address)) }
|
|
547
|
+
pointer_finality_from(resp)
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# Mirrors +Client#pointer_controller+.
|
|
551
|
+
# @return [PointerController]
|
|
552
|
+
def pointer_controller(address)
|
|
553
|
+
resp = grpc_call { @pointer_stub.controller(Antd::V1::PointerControllerRequest.new(address: address)) }
|
|
554
|
+
PointerController.new(pointer: resp.pointer, transfers: resp.transfers.to_a, frozen: resp.frozen)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# Mirrors +Client#pointer_prepare+.
|
|
558
|
+
# @return [PointerPrepareResult]
|
|
559
|
+
def pointer_prepare(owner, op, target: nil, kind: nil, recipient: nil, include_signed_quotes: false)
|
|
560
|
+
req = Antd::V1::PreparePointerRequest.new(
|
|
561
|
+
owner: owner,
|
|
562
|
+
op: op.to_s,
|
|
563
|
+
target: target.to_s,
|
|
564
|
+
kind: kind.to_s,
|
|
565
|
+
recipient: recipient.to_s,
|
|
566
|
+
include_signed_quotes: include_signed_quotes ? true : false
|
|
567
|
+
)
|
|
568
|
+
resp = grpc_call { @pointer_stub.prepare(req) }
|
|
569
|
+
PointerPrepareResult.new(
|
|
570
|
+
upload_id: resp.upload_id,
|
|
571
|
+
op: resp.op,
|
|
572
|
+
address: resp.address,
|
|
573
|
+
state_id: resp.state_id,
|
|
574
|
+
counter: Antd.parse_pointer_counter("counter", resp.counter),
|
|
575
|
+
payment_type: resp.payment_type,
|
|
576
|
+
payments: resp.payments.map do |p|
|
|
577
|
+
PaymentInfo.new(quote_hash: p.quote_hash, rewards_address: p.rewards_address, amount: p.amount)
|
|
578
|
+
end,
|
|
579
|
+
total_amount: resp.total_amount,
|
|
580
|
+
payment_vault_address: resp.payment_vault_address,
|
|
581
|
+
payment_token_address: resp.payment_token_address,
|
|
582
|
+
rpc_url: resp.rpc_url,
|
|
583
|
+
# Bytes on the wire; base64 here, the same form REST returns.
|
|
584
|
+
signed_quotes: resp.signed_quotes.map do |q|
|
|
585
|
+
SignedQuoteEntry.new(
|
|
586
|
+
quote_hash: q.quote_hash,
|
|
587
|
+
quote: Base64.strict_encode64(q.quote),
|
|
588
|
+
commitment_sidecar: q.commitment_sidecar.empty? ? nil : Base64.strict_encode64(q.commitment_sidecar)
|
|
589
|
+
)
|
|
590
|
+
end
|
|
591
|
+
)
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# Mirrors +Client#pointer_finalize+. A +CloseGroupShortfallError+ with
|
|
595
|
+
# +retryable+ means the daemon kept the paid state: repeat the call with
|
|
596
|
+
# the same +upload_id+.
|
|
597
|
+
# @return [PointerFinalizeResult]
|
|
598
|
+
def pointer_finalize(upload_id, tx_hashes)
|
|
599
|
+
req = Antd::V1::FinalizePointerRequest.new(upload_id: upload_id, tx_hashes: tx_hashes)
|
|
600
|
+
resp = grpc_call { @pointer_stub.finalize(req) }
|
|
601
|
+
PointerFinalizeResult.new(
|
|
602
|
+
address: resp.address,
|
|
603
|
+
state_id: resp.state_id,
|
|
604
|
+
counter: Antd.parse_pointer_counter("counter", resp.counter),
|
|
605
|
+
finality: resp.finality.nil? ? nil : pointer_finality_from(resp.finality)
|
|
606
|
+
)
|
|
607
|
+
end
|
|
608
|
+
|
|
471
609
|
private
|
|
472
610
|
|
|
611
|
+
def pointer_write_result(resp)
|
|
612
|
+
PointerWriteResult.new(
|
|
613
|
+
address: resp.address,
|
|
614
|
+
state_id: resp.state_id,
|
|
615
|
+
counter: Antd.parse_pointer_counter("counter", resp.counter)
|
|
616
|
+
)
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
# Converts the proto finality, where an unset counter or transferred_to is
|
|
620
|
+
# "" and an unset state or majority an absent message: each reads as nil.
|
|
621
|
+
def pointer_finality_from(f)
|
|
622
|
+
PointerFinality.new(
|
|
623
|
+
address: f.address,
|
|
624
|
+
group: f.group,
|
|
625
|
+
answered: f.answered,
|
|
626
|
+
status: f.status,
|
|
627
|
+
counter: f.counter.empty? ? nil : Antd.parse_pointer_counter("finality counter", f.counter),
|
|
628
|
+
state: f.state.nil? ? nil : pointer_final_state_from(f.state),
|
|
629
|
+
states: f.states.map { |s| pointer_final_state_from(s) },
|
|
630
|
+
majority: f.majority.nil? ? nil : pointer_final_state_from(f.majority),
|
|
631
|
+
transferred_to: presence(f.transferred_to)
|
|
632
|
+
)
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def pointer_final_state_from(s)
|
|
636
|
+
PointerFinalState.new(
|
|
637
|
+
state_id: s.state_id,
|
|
638
|
+
kind: s.kind,
|
|
639
|
+
kind_tag: s.kind_tag,
|
|
640
|
+
target: s.target,
|
|
641
|
+
holders: s.holders,
|
|
642
|
+
transferred_to: presence(s.transferred_to)
|
|
643
|
+
)
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
# A proto string, or nil when unset (empty).
|
|
647
|
+
def presence(value)
|
|
648
|
+
value.nil? || value.empty? ? nil : value
|
|
649
|
+
end
|
|
650
|
+
|
|
473
651
|
# Drives a +return_op: true+ server-streaming call, yielding +DownloadFrame+s.
|
|
474
652
|
# The byte denominator is surfaced first as a leading +DownloadFrame+ (its
|
|
475
653
|
# +meta+ set), read from the response's +x-content-length+ initial metadata;
|
|
@@ -560,8 +738,21 @@ module Antd
|
|
|
560
738
|
|
|
561
739
|
|
|
562
740
|
# Executes a gRPC call and translates errors to Antd error types.
|
|
741
|
+
#
|
|
742
|
+
# The daemon's +x-antd-error-code+ trailer comes first: where it names a
|
|
743
|
+
# code that shares its gRPC status with older errors (POINTER_FINAL,
|
|
744
|
+
# POINTER_FORKED, CONFLICT, CLOSE_GROUP_SHORTFALL) and agrees with the
|
|
745
|
+
# status code, it selects the typed error. Anything else is re-raised to
|
|
746
|
+
# the status-code mapping below.
|
|
563
747
|
def grpc_call
|
|
564
|
-
|
|
748
|
+
begin
|
|
749
|
+
yield
|
|
750
|
+
rescue GRPC::BadStatus => e
|
|
751
|
+
tagged = Antd.error_for_grpc_metadata(e.code, e.details, e.message, e.metadata)
|
|
752
|
+
raise tagged unless tagged.nil?
|
|
753
|
+
|
|
754
|
+
raise
|
|
755
|
+
end
|
|
565
756
|
rescue GRPC::InvalidArgument => e
|
|
566
757
|
raise BadRequestError, e.message
|
|
567
758
|
rescue GRPC::NotFound => e
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "manifests"
|
|
4
|
+
require_relative "grpc_client"
|
|
5
|
+
require_relative "v1/manifests_services_pb"
|
|
6
|
+
|
|
7
|
+
# The manifest methods on +Antd::GrpcClient+, over ManifestService. The
|
|
8
|
+
# models and the entry check are in manifests.rb, shared with REST.
|
|
9
|
+
module Antd
|
|
10
|
+
# Channel arguments for the manifest stub's own channel. grpc-ruby has no
|
|
11
|
+
# per-call message limit, so ManifestService gets a channel of its own
|
|
12
|
+
# with both limits raised to +MANIFEST_GRPC_MAX_MESSAGE+; the other
|
|
13
|
+
# services keep gRPC's defaults.
|
|
14
|
+
MANIFEST_GRPC_CHANNEL_ARGS = {
|
|
15
|
+
"grpc.max_receive_message_length" => MANIFEST_GRPC_MAX_MESSAGE,
|
|
16
|
+
"grpc.max_send_message_length" => MANIFEST_GRPC_MAX_MESSAGE
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
class GrpcClient
|
|
20
|
+
# --- Manifests ---
|
|
21
|
+
#
|
|
22
|
+
# The same methods and models as the REST +Client+ (see there). The
|
|
23
|
+
# `.ant` bytes travel raw over gRPC; REST's base64 never shows. Unset
|
|
24
|
+
# optionals (proto3 +optional+ without a value, or an empty string) read
|
|
25
|
+
# as nil, as REST's omitted fields do.
|
|
26
|
+
|
|
27
|
+
# Mirrors +Client#build_manifest+. Nothing is paid for.
|
|
28
|
+
#
|
|
29
|
+
# Needs antd 0.16.0 or later.
|
|
30
|
+
#
|
|
31
|
+
# @return [BuiltManifest]
|
|
32
|
+
# @raise [BadRequestError] an entry with both or neither of +data_map+ and
|
|
33
|
+
# +address+, or a bad +size+, before anything is sent
|
|
34
|
+
# @raise [BadRequestError] an entry the daemon rejects (INVALID_ARGUMENT);
|
|
35
|
+
# the message names +entries[i]+
|
|
36
|
+
def build_manifest(entries, name: nil, torrent_info_hashes: nil, embed_roots: true)
|
|
37
|
+
inputs = Antd.manifest_entry_inputs(entries)
|
|
38
|
+
req = Antd::V1::BuildManifestRequest.new(
|
|
39
|
+
torrent_info_hashes: Array(torrent_info_hashes),
|
|
40
|
+
# Always sent, so "unset means true" never depends on the daemon.
|
|
41
|
+
embed_roots: embed_roots ? true : false,
|
|
42
|
+
entries: inputs.map { |e| manifest_entry_input_pb(e) }
|
|
43
|
+
)
|
|
44
|
+
req.name = name unless name.nil? || name.empty?
|
|
45
|
+
|
|
46
|
+
resp = grpc_call { manifest_stub.build(req) }
|
|
47
|
+
BuiltManifest.new(
|
|
48
|
+
manifest: resp.manifest,
|
|
49
|
+
link: manifest_pb_string(resp.link),
|
|
50
|
+
entries: resp.entries.map { |e| manifest_entry_info_from_pb(e) }
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Mirrors +Client#decode_manifest+. The bytes are sent raw.
|
|
55
|
+
#
|
|
56
|
+
# Needs antd 0.16.0 or later.
|
|
57
|
+
#
|
|
58
|
+
# @return [DecodedManifest]
|
|
59
|
+
def decode_manifest(manifest, resolve: false)
|
|
60
|
+
raise BadRequestError, MANIFEST_DECODE_NEEDS_INPUT if manifest.nil? || manifest.empty?
|
|
61
|
+
|
|
62
|
+
decode_manifest_pb(Antd::V1::DecodeManifestRequest.new(manifest: manifest.b, resolve: resolve ? true : false))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Mirrors +Client#decode_manifest_link+.
|
|
66
|
+
#
|
|
67
|
+
# Needs antd 0.16.0 or later.
|
|
68
|
+
#
|
|
69
|
+
# @return [DecodedManifest]
|
|
70
|
+
def decode_manifest_link(link, resolve: false)
|
|
71
|
+
raise BadRequestError, MANIFEST_DECODE_NEEDS_INPUT if link.nil? || link.empty?
|
|
72
|
+
|
|
73
|
+
decode_manifest_pb(Antd::V1::DecodeManifestRequest.new(link: link, resolve: resolve ? true : false))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# ManifestService on its own channel, with +MANIFEST_GRPC_CHANNEL_ARGS+.
|
|
79
|
+
# Made on first use, so +initialize+ stays as it is.
|
|
80
|
+
def manifest_stub
|
|
81
|
+
@manifest_stub ||= Antd::V1::ManifestService::Stub.new(
|
|
82
|
+
@target, :this_channel_is_insecure, channel_args: MANIFEST_GRPC_CHANNEL_ARGS
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def decode_manifest_pb(req)
|
|
87
|
+
resp = grpc_call { manifest_stub.decode(req) }
|
|
88
|
+
DecodedManifest.new(
|
|
89
|
+
source: resp.source,
|
|
90
|
+
name: resp.has_name? ? manifest_pb_string(resp.name) : nil,
|
|
91
|
+
torrent_info_hash_v1: manifest_pb_string(resp.torrent_info_hash_v1),
|
|
92
|
+
torrent_info_hash_v2: manifest_pb_string(resp.torrent_info_hash_v2),
|
|
93
|
+
entries: resp.entries.map { |e| manifest_entry_info_from_pb(e) }
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def manifest_entry_input_pb(e)
|
|
98
|
+
attrs = {}
|
|
99
|
+
attrs[:path] = e.path unless e.path.nil? || e.path.empty?
|
|
100
|
+
attrs[:size] = e.size unless e.size.nil?
|
|
101
|
+
if e.data_map.nil? || e.data_map.empty?
|
|
102
|
+
attrs[:address] = e.address
|
|
103
|
+
else
|
|
104
|
+
attrs[:data_map] = e.data_map
|
|
105
|
+
end
|
|
106
|
+
Antd::V1::ManifestEntryInput.new(**attrs)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# proto3 +optional+ sizes are checked for presence: unset is nil, not 0.
|
|
110
|
+
def manifest_entry_info_from_pb(e)
|
|
111
|
+
ManifestEntryInfo.new(
|
|
112
|
+
effective_name: e.effective_name,
|
|
113
|
+
path: e.has_path? ? manifest_pb_string(e.path) : nil,
|
|
114
|
+
size: e.has_size? ? e.size : nil,
|
|
115
|
+
kind: e.kind,
|
|
116
|
+
content_address: e.content_address,
|
|
117
|
+
data_map: manifest_pb_string(e.data_map),
|
|
118
|
+
exact_size: e.has_exact_size? ? e.exact_size : nil,
|
|
119
|
+
error: manifest_pb_string(e.error)
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# A proto string, or nil when empty.
|
|
124
|
+
def manifest_pb_string(value)
|
|
125
|
+
value.nil? || value.empty? ? nil : value
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require_relative "models"
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
require_relative "client"
|
|
7
|
+
|
|
8
|
+
# Manifests (ADR-0006): `.ant` files and `ant://` links that describe a set
|
|
9
|
+
# of files, each with an optional path and size and either an embedded
|
|
10
|
+
# DataMap or a public DataMap address. The daemon builds and decodes them
|
|
11
|
+
# through ant-core, so callers never re-implement the format. Nothing is
|
|
12
|
+
# uploaded or paid for.
|
|
13
|
+
#
|
|
14
|
+
# This file holds the models and the REST methods on +Antd::Client+;
|
|
15
|
+
# grpc_manifests.rb adds the same methods to +Antd::GrpcClient+.
|
|
16
|
+
module Antd
|
|
17
|
+
# +ManifestEntryInfo#kind+ values.
|
|
18
|
+
module ManifestEntryKind
|
|
19
|
+
# The manifest carries the entry's DataMap: it is the key to the file.
|
|
20
|
+
EMBEDDED = "embedded"
|
|
21
|
+
# The manifest carries a public file's DataMap address.
|
|
22
|
+
PUBLIC = "public"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# +DecodedManifest#source+ values.
|
|
26
|
+
module ManifestSource
|
|
27
|
+
# `.ant` bytes or an `ant://manifest/` link.
|
|
28
|
+
MANIFEST = "manifest"
|
|
29
|
+
# A file link or bare address, reported as one public entry with no path.
|
|
30
|
+
FILE_LINK = "file_link"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# One file for +build_manifest+. Set exactly one of +data_map+ or
|
|
34
|
+
# +address+; a Hash with the same keys works too.
|
|
35
|
+
#
|
|
36
|
+
# - +path+: the relative path the entry extracts under, subject to the
|
|
37
|
+
# portable path rules. nil: it extracts under its content address.
|
|
38
|
+
# - +size+: the plaintext size to record, as given (the creator's claim).
|
|
39
|
+
# nil: none is recorded; readers take the exact size from an embedded
|
|
40
|
+
# root DataMap.
|
|
41
|
+
# - +data_map+: a hex DataMap, as the upload methods return it. The entry
|
|
42
|
+
# embeds it.
|
|
43
|
+
# - +address+: a public file's DataMap address (hex, 32 bytes).
|
|
44
|
+
ManifestEntryInput = Struct.new(:path, :size, :data_map, :address, keyword_init: true) do
|
|
45
|
+
def initialize(path: nil, size: nil, data_map: nil, address: nil)
|
|
46
|
+
super
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# What a manifest says about one entry. Unset fields are nil on both
|
|
51
|
+
# transports (REST omits them; gRPC sends no value or an empty string),
|
|
52
|
+
# so a +size+ or +exact_size+ the manifest lacks is nil, never 0.
|
|
53
|
+
#
|
|
54
|
+
# - +effective_name+: +path+, or the content address in hex when there is
|
|
55
|
+
# none.
|
|
56
|
+
# - +path+: the entry's path, or nil.
|
|
57
|
+
# - +size+: the size recorded in the manifest (the creator's claim), or nil.
|
|
58
|
+
# - +kind+: a +ManifestEntryKind+ string.
|
|
59
|
+
# - +content_address+: hex. The file's public DataMap address, whether or
|
|
60
|
+
# not it was ever made public; the same for private and public uploads
|
|
61
|
+
# of one file.
|
|
62
|
+
# - +data_map+: the embedded DataMap in the hex form the download methods
|
|
63
|
+
# accept. Decode only; nil for public entries.
|
|
64
|
+
# - +exact_size+: the exact plaintext size, from an embedded root DataMap
|
|
65
|
+
# or after a resolving decode; nil when unknown.
|
|
66
|
+
# - +error+: why a resolving decode failed for this entry, or nil. A
|
|
67
|
+
# failed entry does not fail the call.
|
|
68
|
+
ManifestEntryInfo = Struct.new(
|
|
69
|
+
:effective_name, :path, :size, :kind, :content_address, :data_map, :exact_size, :error,
|
|
70
|
+
keyword_init: true
|
|
71
|
+
) do
|
|
72
|
+
def initialize(effective_name: "", path: nil, size: nil, kind: "", content_address: "",
|
|
73
|
+
data_map: nil, exact_size: nil, error: nil)
|
|
74
|
+
super
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Result of +build_manifest+.
|
|
79
|
+
#
|
|
80
|
+
# - +manifest+: the `.ant` bytes (a binary String).
|
|
81
|
+
# - +link+: the `ant://manifest/` link, or nil when it would not fit the
|
|
82
|
+
# recommended 1,500 bytes.
|
|
83
|
+
# - +entries+: +ManifestEntryInfo+s in encoded order (sorted by effective
|
|
84
|
+
# name).
|
|
85
|
+
BuiltManifest = Struct.new(:manifest, :link, :entries, keyword_init: true)
|
|
86
|
+
|
|
87
|
+
# Result of +decode_manifest+ and +decode_manifest_link+.
|
|
88
|
+
#
|
|
89
|
+
# - +source+: a +ManifestSource+ string.
|
|
90
|
+
# - +name+: the suggested root directory name, or nil.
|
|
91
|
+
# - +torrent_info_hash_v1+, +torrent_info_hash_v2+: hex, or nil when absent.
|
|
92
|
+
# - +entries+: +ManifestEntryInfo+s.
|
|
93
|
+
DecodedManifest = Struct.new(
|
|
94
|
+
:source, :name, :torrent_info_hash_v1, :torrent_info_hash_v2, :entries,
|
|
95
|
+
keyword_init: true
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# The gRPC message ceiling for manifest calls, both ways: a manifest can be
|
|
99
|
+
# 64 MiB, and a decode response carries its DataMaps in hex. gRPC's default
|
|
100
|
+
# receive limit is 4 MiB.
|
|
101
|
+
MANIFEST_GRPC_MAX_MESSAGE = 256 * 1024 * 1024
|
|
102
|
+
|
|
103
|
+
# The daemon's cap on the entry errors one 400 names.
|
|
104
|
+
MANIFEST_MAX_REPORTED_ENTRY_ERRORS = 20
|
|
105
|
+
private_constant :MANIFEST_MAX_REPORTED_ENTRY_ERRORS
|
|
106
|
+
|
|
107
|
+
# The daemon's message for empty decode input (empty bytes or link).
|
|
108
|
+
MANIFEST_DECODE_NEEDS_INPUT = "needs a manifest (base64 .ant bytes) or a link"
|
|
109
|
+
|
|
110
|
+
# Normalises +build_manifest+ entries to +ManifestEntryInput+s before
|
|
111
|
+
# anything is sent. A refusal is the +BadRequestError+ (400) the daemon
|
|
112
|
+
# would send, with its wording: every bad entry (both or neither of
|
|
113
|
+
# +data_map+ and +address+, nil and "" counting as unset; or a +size+ that
|
|
114
|
+
# is not a non-negative Integer) is collected and named as
|
|
115
|
+
# "N invalid entries: entries[i]: ...; entries[j]: ..." (at most 20, then
|
|
116
|
+
# "; and K more").
|
|
117
|
+
#
|
|
118
|
+
# @api private
|
|
119
|
+
# @param entries [Array<ManifestEntryInput, Hash>]
|
|
120
|
+
# @return [Array<ManifestEntryInput>]
|
|
121
|
+
# @raise [BadRequestError]
|
|
122
|
+
def self.manifest_entry_inputs(entries)
|
|
123
|
+
raise BadRequestError, "entries must be an Array" unless entries.is_a?(Array)
|
|
124
|
+
|
|
125
|
+
errors = []
|
|
126
|
+
inputs = entries.each_with_index.map do |e, i|
|
|
127
|
+
e = ManifestEntryInput.new(**e.transform_keys(&:to_sym)) if e.is_a?(Hash)
|
|
128
|
+
unless e.is_a?(ManifestEntryInput)
|
|
129
|
+
errors << "entries[#{i}]: must be a ManifestEntryInput or Hash"
|
|
130
|
+
next
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
has_data_map = !(e.data_map.nil? || e.data_map.empty?)
|
|
134
|
+
has_address = !(e.address.nil? || e.address.empty?)
|
|
135
|
+
if has_data_map && has_address
|
|
136
|
+
errors << "entries[#{i}]: give data_map or address, not both"
|
|
137
|
+
elsif !has_data_map && !has_address
|
|
138
|
+
errors << "entries[#{i}]: needs a data_map or an address"
|
|
139
|
+
end
|
|
140
|
+
unless e.size.nil? || (e.size.is_a?(Integer) && e.size >= 0)
|
|
141
|
+
errors << "entries[#{i}]: size must be a non-negative Integer or nil"
|
|
142
|
+
end
|
|
143
|
+
e
|
|
144
|
+
rescue ArgumentError
|
|
145
|
+
# A Hash with a key ManifestEntryInput does not have.
|
|
146
|
+
errors << "entries[#{i}]: unknown field (expected path, size, data_map, address)"
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
raise BadRequestError, Antd.manifest_entry_errors_message(errors) unless errors.empty?
|
|
150
|
+
|
|
151
|
+
inputs
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# The daemon's +entry_errors_message+.
|
|
155
|
+
#
|
|
156
|
+
# @api private
|
|
157
|
+
def self.manifest_entry_errors_message(errors)
|
|
158
|
+
shown = errors.first(MANIFEST_MAX_REPORTED_ENTRY_ERRORS).join("; ")
|
|
159
|
+
message = "#{errors.length} invalid entries: #{shown}"
|
|
160
|
+
message += "; and #{errors.length - MANIFEST_MAX_REPORTED_ENTRY_ERRORS} more" if errors.length > MANIFEST_MAX_REPORTED_ENTRY_ERRORS
|
|
161
|
+
message
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
class Client
|
|
165
|
+
# --- Manifests ---
|
|
166
|
+
|
|
167
|
+
# Build a `.ant` manifest from DataMaps and public addresses already in
|
|
168
|
+
# hand (POST /v1/manifests/build). Nothing is uploaded or paid for.
|
|
169
|
+
#
|
|
170
|
+
# By default the daemon resolves each shrunk DataMap's root (fetching its
|
|
171
|
+
# wrapper records) and embeds it when it is at most 64 KiB, so recipients
|
|
172
|
+
# skip that fetch (ADR-0006's policy). That reads from the network, so the
|
|
173
|
+
# call can time out; it is safe to retry.
|
|
174
|
+
#
|
|
175
|
+
# Needs antd 0.16.0 or later.
|
|
176
|
+
#
|
|
177
|
+
# @param entries [Array<ManifestEntryInput, Hash>] each with exactly one
|
|
178
|
+
# of +data_map+ or +address+; checked before anything is sent
|
|
179
|
+
# @param name [String, nil] suggested root directory name (one portable
|
|
180
|
+
# path component)
|
|
181
|
+
# @param torrent_info_hashes [Array<String>, nil] BitTorrent info hashes
|
|
182
|
+
# of the same files: hex, 40 characters for v1 and/or 64 for v2
|
|
183
|
+
# @param embed_roots [Boolean] false embeds shrunk DataMaps exactly as
|
|
184
|
+
# given (the wire's +embed_roots+; default true)
|
|
185
|
+
# @return [BuiltManifest]
|
|
186
|
+
# @raise [BadRequestError] an entry with both or neither of +data_map+ and
|
|
187
|
+
# +address+, or a bad +size+, worded as the daemon words it
|
|
188
|
+
# @raise [BadRequestError] an entry the daemon rejects; the message names
|
|
189
|
+
# +entries[i]+
|
|
190
|
+
def build_manifest(entries, name: nil, torrent_info_hashes: nil, embed_roots: true)
|
|
191
|
+
inputs = Antd.manifest_entry_inputs(entries)
|
|
192
|
+
wire = inputs.map do |e|
|
|
193
|
+
m = {}
|
|
194
|
+
m[:path] = e.path unless e.path.nil? || e.path.empty?
|
|
195
|
+
m[:size] = e.size unless e.size.nil?
|
|
196
|
+
if e.data_map.nil? || e.data_map.empty?
|
|
197
|
+
m[:address] = e.address
|
|
198
|
+
else
|
|
199
|
+
m[:data_map] = e.data_map
|
|
200
|
+
end
|
|
201
|
+
m
|
|
202
|
+
end
|
|
203
|
+
body = { entries: wire, embed_roots: embed_roots ? true : false }
|
|
204
|
+
body[:name] = name unless name.nil? || name.empty?
|
|
205
|
+
hashes = Array(torrent_info_hashes)
|
|
206
|
+
body[:torrent_info_hashes] = hashes unless hashes.empty?
|
|
207
|
+
|
|
208
|
+
j = do_json(:post, "/v1/manifests/build", body)
|
|
209
|
+
BuiltManifest.new(
|
|
210
|
+
manifest: manifest_bytes_from_b64(j["manifest"]),
|
|
211
|
+
link: manifest_string(j["link"]),
|
|
212
|
+
entries: manifest_entry_infos(j["entries"])
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Decode `.ant` bytes (POST /v1/manifests/decode). With +resolve+, the
|
|
217
|
+
# daemon fetches each entry's DataMap (and wrapper records) to report
|
|
218
|
+
# +exact_size+ and confirm the DataMap is on the network; content chunks
|
|
219
|
+
# are not fetched, and one entry's failure is reported in its +error+,
|
|
220
|
+
# not as a failed call. Nothing is paid for; safe to retry.
|
|
221
|
+
#
|
|
222
|
+
# Needs antd 0.16.0 or later.
|
|
223
|
+
#
|
|
224
|
+
# @param manifest [String] the `.ant` bytes
|
|
225
|
+
# @param resolve [Boolean]
|
|
226
|
+
# @return [DecodedManifest]
|
|
227
|
+
# @raise [BadRequestError] an empty +manifest+
|
|
228
|
+
# @raise [BadRequestError] bytes that are not a manifest
|
|
229
|
+
def decode_manifest(manifest, resolve: false)
|
|
230
|
+
raise BadRequestError, MANIFEST_DECODE_NEEDS_INPUT if manifest.nil? || manifest.empty?
|
|
231
|
+
|
|
232
|
+
post_manifest_decode({ manifest: b64_encode(manifest), resolve: resolve ? true : false })
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Decode an `ant://` manifest link, file link or bare 64-hex address
|
|
236
|
+
# (POST /v1/manifests/decode). See +decode_manifest+ for +resolve+.
|
|
237
|
+
#
|
|
238
|
+
# Needs antd 0.16.0 or later.
|
|
239
|
+
#
|
|
240
|
+
# @param link [String]
|
|
241
|
+
# @param resolve [Boolean]
|
|
242
|
+
# @return [DecodedManifest]
|
|
243
|
+
# @raise [BadRequestError] an empty +link+
|
|
244
|
+
# @raise [BadRequestError] a link the daemon cannot read
|
|
245
|
+
def decode_manifest_link(link, resolve: false)
|
|
246
|
+
raise BadRequestError, MANIFEST_DECODE_NEEDS_INPUT if link.nil? || link.empty?
|
|
247
|
+
|
|
248
|
+
post_manifest_decode({ link: link, resolve: resolve ? true : false })
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
private
|
|
252
|
+
|
|
253
|
+
def post_manifest_decode(body)
|
|
254
|
+
j = do_json(:post, "/v1/manifests/decode", body)
|
|
255
|
+
DecodedManifest.new(
|
|
256
|
+
source: j["source"] || "",
|
|
257
|
+
name: manifest_string(j["name"]),
|
|
258
|
+
torrent_info_hash_v1: manifest_string(j["torrent_info_hash_v1"]),
|
|
259
|
+
torrent_info_hash_v2: manifest_string(j["torrent_info_hash_v2"]),
|
|
260
|
+
entries: manifest_entry_infos(j["entries"])
|
|
261
|
+
)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# The build response's base64 `.ant` bytes, raw.
|
|
265
|
+
def manifest_bytes_from_b64(value)
|
|
266
|
+
b64_decode(value.to_s)
|
|
267
|
+
rescue ArgumentError => e
|
|
268
|
+
raise InternalError, "build_manifest: manifest is not valid base64: #{e.message}"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def manifest_entry_infos(raw)
|
|
272
|
+
return [] unless raw.is_a?(Array)
|
|
273
|
+
|
|
274
|
+
raw.filter_map do |e|
|
|
275
|
+
next unless e.is_a?(Hash)
|
|
276
|
+
|
|
277
|
+
ManifestEntryInfo.new(
|
|
278
|
+
effective_name: e["effective_name"] || "",
|
|
279
|
+
path: manifest_string(e["path"]),
|
|
280
|
+
size: manifest_size(e["size"]),
|
|
281
|
+
kind: e["kind"] || "",
|
|
282
|
+
content_address: e["content_address"] || "",
|
|
283
|
+
data_map: manifest_string(e["data_map"]),
|
|
284
|
+
exact_size: manifest_size(e["exact_size"]),
|
|
285
|
+
error: manifest_string(e["error"])
|
|
286
|
+
)
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# An optional size: a JSON integer, or nil when absent (never 0).
|
|
291
|
+
def manifest_size(value)
|
|
292
|
+
value.is_a?(Integer) && value >= 0 ? value : nil
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# An optional string: nil when absent or empty.
|
|
296
|
+
def manifest_string(value)
|
|
297
|
+
value.is_a?(String) && !value.empty? ? value : nil
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|