create-sia-app 0.1.1
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.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +179 -0
- package/package.json +29 -0
- package/template/CLAUDE.md +160 -0
- package/template/README.md +102 -0
- package/template/_gitignore +5 -0
- package/template/biome.json +40 -0
- package/template/index.html +13 -0
- package/template/package.json +30 -0
- package/template/rust/README.md +16 -0
- package/template/rust/sia-sdk-rs/.changeset/added_cancel_function_to_cancel_inflight_packed_uploads.md +6 -0
- package/template/rust/sia-sdk-rs/.changeset/check_if_we_have_enough_hosts_prior_to_encoding_in_upload_slabs.md +16 -0
- package/template/rust/sia-sdk-rs/.changeset/fix_slab_length_in_packed_object.md +5 -0
- package/template/rust/sia-sdk-rs/.changeset/fix_upload_racing_race_conditon.md +13 -0
- package/template/rust/sia-sdk-rs/.changeset/improved_parallelism_of_packed_uploads.md +5 -0
- package/template/rust/sia-sdk-rs/.changeset/progress_callback_will_now_be_called_as_expected_for_packed_uploads.md +5 -0
- package/template/rust/sia-sdk-rs/.github/dependabot.yml +10 -0
- package/template/rust/sia-sdk-rs/.github/workflows/main.yml +36 -0
- package/template/rust/sia-sdk-rs/.github/workflows/prepare-release.yml +34 -0
- package/template/rust/sia-sdk-rs/.github/workflows/release.yml +30 -0
- package/template/rust/sia-sdk-rs/.rustfmt.toml +4 -0
- package/template/rust/sia-sdk-rs/Cargo.lock +4127 -0
- package/template/rust/sia-sdk-rs/Cargo.toml +3 -0
- package/template/rust/sia-sdk-rs/LICENSE +21 -0
- package/template/rust/sia-sdk-rs/README.md +30 -0
- package/template/rust/sia-sdk-rs/indexd/CHANGELOG.md +79 -0
- package/template/rust/sia-sdk-rs/indexd/Cargo.toml +79 -0
- package/template/rust/sia-sdk-rs/indexd/benches/upload.rs +258 -0
- package/template/rust/sia-sdk-rs/indexd/src/app_client.rs +1710 -0
- package/template/rust/sia-sdk-rs/indexd/src/builder.rs +354 -0
- package/template/rust/sia-sdk-rs/indexd/src/download.rs +379 -0
- package/template/rust/sia-sdk-rs/indexd/src/hosts.rs +659 -0
- package/template/rust/sia-sdk-rs/indexd/src/lib.rs +827 -0
- package/template/rust/sia-sdk-rs/indexd/src/mock.rs +162 -0
- package/template/rust/sia-sdk-rs/indexd/src/object_encryption.rs +125 -0
- package/template/rust/sia-sdk-rs/indexd/src/quic.rs +575 -0
- package/template/rust/sia-sdk-rs/indexd/src/rhp4.rs +52 -0
- package/template/rust/sia-sdk-rs/indexd/src/slabs.rs +497 -0
- package/template/rust/sia-sdk-rs/indexd/src/upload.rs +629 -0
- package/template/rust/sia-sdk-rs/indexd/src/wasm_time.rs +41 -0
- package/template/rust/sia-sdk-rs/indexd/src/web_transport.rs +398 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/CHANGELOG.md +76 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/Cargo.toml +47 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/examples/python/README.md +10 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/examples/python/example.py +130 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/src/bin/uniffi-bindgen.rs +3 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/src/builder.rs +377 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/src/io.rs +155 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/src/lib.rs +1039 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/src/logging.rs +58 -0
- package/template/rust/sia-sdk-rs/indexd_ffi/src/tls.rs +23 -0
- package/template/rust/sia-sdk-rs/indexd_wasm/Cargo.toml +33 -0
- package/template/rust/sia-sdk-rs/indexd_wasm/src/lib.rs +818 -0
- package/template/rust/sia-sdk-rs/knope.toml +54 -0
- package/template/rust/sia-sdk-rs/sia_derive/CHANGELOG.md +38 -0
- package/template/rust/sia-sdk-rs/sia_derive/Cargo.toml +19 -0
- package/template/rust/sia-sdk-rs/sia_derive/src/lib.rs +278 -0
- package/template/rust/sia-sdk-rs/sia_sdk/CHANGELOG.md +91 -0
- package/template/rust/sia-sdk-rs/sia_sdk/Cargo.toml +59 -0
- package/template/rust/sia-sdk-rs/sia_sdk/benches/merkle_root.rs +12 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/blake2.rs +22 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/consensus.rs +767 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/encoding/v1.rs +257 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/encoding/v2.rs +291 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/encoding.rs +26 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/encoding_async/v2.rs +367 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/encoding_async.rs +6 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/encryption.rs +303 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/erasure_coding.rs +347 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/lib.rs +15 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/macros.rs +435 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/merkle.rs +112 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/rhp/merkle.rs +357 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/rhp/rpc.rs +1507 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/rhp/types.rs +146 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/rhp.rs +7 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/seed.rs +278 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/signing.rs +236 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/common.rs +677 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/currency.rs +450 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/specifier.rs +110 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/spendpolicy.rs +778 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/utils.rs +117 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/v1.rs +1737 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/v2.rs +1726 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types/work.rs +59 -0
- package/template/rust/sia-sdk-rs/sia_sdk/src/types.rs +16 -0
- package/template/scripts/setup-rust.js +29 -0
- package/template/src/App.tsx +13 -0
- package/template/src/components/DevNote.tsx +21 -0
- package/template/src/components/auth/ApproveScreen.tsx +84 -0
- package/template/src/components/auth/AuthFlow.tsx +77 -0
- package/template/src/components/auth/ConnectScreen.tsx +214 -0
- package/template/src/components/auth/LoadingScreen.tsx +8 -0
- package/template/src/components/auth/RecoveryScreen.tsx +182 -0
- package/template/src/components/upload/UploadZone.tsx +314 -0
- package/template/src/index.css +9 -0
- package/template/src/lib/constants.ts +8 -0
- package/template/src/lib/format.ts +35 -0
- package/template/src/lib/hex.ts +13 -0
- package/template/src/lib/sdk.ts +25 -0
- package/template/src/lib/wasm-env.ts +5 -0
- package/template/src/main.tsx +12 -0
- package/template/src/stores/auth.ts +86 -0
- package/template/tsconfig.app.json +31 -0
- package/template/tsconfig.json +7 -0
- package/template/tsconfig.node.json +26 -0
- package/template/vite.config.ts +18 -0
- package/template/wasm/indexd_wasm/indexd_wasm.d.ts +309 -0
- package/template/wasm/indexd_wasm/indexd_wasm.js +1507 -0
- package/template/wasm/indexd_wasm/indexd_wasm_bg.wasm +0 -0
- package/template/wasm/indexd_wasm/package.json +31 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
use std::io;
|
|
2
|
+
use std::sync::Arc;
|
|
3
|
+
use std::time::Duration;
|
|
4
|
+
|
|
5
|
+
use bytes::Bytes;
|
|
6
|
+
use log::debug;
|
|
7
|
+
use sia::encryption::{EncryptionKey, encrypt_shard};
|
|
8
|
+
use sia::erasure_coding::{self, ErasureCoder};
|
|
9
|
+
use sia::rhp::{self, SECTOR_SIZE};
|
|
10
|
+
use sia::signing::{PrivateKey, PublicKey};
|
|
11
|
+
use thiserror::Error;
|
|
12
|
+
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader, SimplexStream, WriteHalf, copy, simplex};
|
|
13
|
+
use tokio::sync::{OwnedSemaphorePermit, Semaphore, mpsc};
|
|
14
|
+
use tokio::task::JoinSet;
|
|
15
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
16
|
+
use tokio::task::spawn_blocking;
|
|
17
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
18
|
+
use tokio::time::{Instant, sleep, timeout};
|
|
19
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
20
|
+
use tokio::time::error::Elapsed;
|
|
21
|
+
#[cfg(target_arch = "wasm32")]
|
|
22
|
+
use crate::wasm_time::{Instant, sleep};
|
|
23
|
+
use tokio_util::task::AbortOnDropHandle;
|
|
24
|
+
|
|
25
|
+
use crate::hosts::{HostQueue, QueueError};
|
|
26
|
+
use crate::rhp4::RHP4Client;
|
|
27
|
+
use crate::{Hosts, Object, Sector, Slab};
|
|
28
|
+
|
|
29
|
+
/// Spawns a task on a [`JoinSet`]. Uses `spawn` on native (requires `Send`)
|
|
30
|
+
/// and `spawn_local` on WASM (runs on the current [`tokio::task::LocalSet`]).
|
|
31
|
+
macro_rules! join_set_spawn {
|
|
32
|
+
($set:expr, $fut:expr) => {{
|
|
33
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
34
|
+
$set.spawn($fut);
|
|
35
|
+
#[cfg(target_arch = "wasm32")]
|
|
36
|
+
$set.spawn_local($fut);
|
|
37
|
+
}};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[derive(Debug, Error)]
|
|
41
|
+
pub enum UploadError {
|
|
42
|
+
#[error("invalid options {0}")]
|
|
43
|
+
InvalidOptions(String),
|
|
44
|
+
|
|
45
|
+
#[error("i/o error: {0}")]
|
|
46
|
+
Io(#[from] io::Error),
|
|
47
|
+
|
|
48
|
+
#[error("rhp4 error: {0}")]
|
|
49
|
+
Rhp4(#[from] crate::rhp4::Error),
|
|
50
|
+
|
|
51
|
+
#[error("encoder error: {0}")]
|
|
52
|
+
Encoder(#[from] erasure_coding::Error),
|
|
53
|
+
#[error("not enough shards: {0}/{1}")]
|
|
54
|
+
NotEnoughShards(u8, u8),
|
|
55
|
+
|
|
56
|
+
#[error("invalid range: {0}-{1}")]
|
|
57
|
+
OutOfRange(usize, usize),
|
|
58
|
+
|
|
59
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
60
|
+
#[error("timeout error: {0}")]
|
|
61
|
+
Timeout(#[from] Elapsed),
|
|
62
|
+
|
|
63
|
+
#[error("queue error: {0}")]
|
|
64
|
+
QueueError(#[from] QueueError),
|
|
65
|
+
|
|
66
|
+
#[error("semaphore error: {0}")]
|
|
67
|
+
SemaphoreError(#[from] tokio::sync::AcquireError),
|
|
68
|
+
|
|
69
|
+
#[error("join error: {0}")]
|
|
70
|
+
JoinError(#[from] tokio::task::JoinError),
|
|
71
|
+
|
|
72
|
+
#[error("api error: {0}")]
|
|
73
|
+
ApiError(#[from] crate::app_client::Error),
|
|
74
|
+
|
|
75
|
+
#[error("slab id mismatch")]
|
|
76
|
+
InvalidSlabId,
|
|
77
|
+
|
|
78
|
+
#[error("upload cancelled")]
|
|
79
|
+
Cancelled,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
pub struct UploadOptions {
|
|
83
|
+
pub data_shards: u8,
|
|
84
|
+
pub parity_shards: u8,
|
|
85
|
+
pub max_inflight: usize,
|
|
86
|
+
|
|
87
|
+
/// Optional channel to notify when each shard is uploaded.
|
|
88
|
+
/// This can be used to implement progress reporting.
|
|
89
|
+
pub shard_uploaded: Option<mpsc::UnboundedSender<()>>,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
impl Default for UploadOptions {
|
|
93
|
+
fn default() -> Self {
|
|
94
|
+
Self {
|
|
95
|
+
data_shards: 10,
|
|
96
|
+
parity_shards: 20,
|
|
97
|
+
#[cfg(target_arch = "wasm32")]
|
|
98
|
+
max_inflight: 3, // Browsers can't handle many concurrent WebTransport connections
|
|
99
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
100
|
+
max_inflight: 16,
|
|
101
|
+
shard_uploaded: None,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
struct ObjectUpload {
|
|
107
|
+
start: u64,
|
|
108
|
+
end: u64,
|
|
109
|
+
object: Object,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// A packed upload allows multiple objects to be uploaded together in a single upload. This can be more
|
|
113
|
+
/// efficient than uploading each object separately if the size of the object is less than the minimum
|
|
114
|
+
/// slab size.
|
|
115
|
+
///
|
|
116
|
+
/// The caller must call [finalize](Self::finalize) to complete the upload.
|
|
117
|
+
pub struct PackedUpload {
|
|
118
|
+
slab_size: u64,
|
|
119
|
+
length: u64,
|
|
120
|
+
writer: WriteHalf<SimplexStream>,
|
|
121
|
+
objects: Vec<ObjectUpload>,
|
|
122
|
+
upload_handle: AbortOnDropHandle<Result<Vec<Slab>, UploadError>>,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
impl PackedUpload {
|
|
126
|
+
/// Returns the number of bytes remaining until reaching the optimal
|
|
127
|
+
/// packed size. Adding objects larger than this will start a new slab.
|
|
128
|
+
/// To minimize padding, prioritize objects that fit within the
|
|
129
|
+
/// remaining size.
|
|
130
|
+
pub fn remaining(&self) -> u64 {
|
|
131
|
+
if self.length == 0 {
|
|
132
|
+
return self.slab_size;
|
|
133
|
+
}
|
|
134
|
+
(self.slab_size - (self.length % self.slab_size)) % self.slab_size
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Returns the cumulative length of all objects currently in the upload.
|
|
138
|
+
pub fn length(&self) -> u64 {
|
|
139
|
+
self.length
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// Returns the optimal size of each slab.
|
|
143
|
+
pub fn slab_size(&self) -> u64 {
|
|
144
|
+
self.slab_size
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Returns the number of slabs after the upload is finalized.
|
|
148
|
+
pub fn slabs(&self) -> u64 {
|
|
149
|
+
self.length.div_ceil(self.slab_size)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Cancels the upload.
|
|
153
|
+
pub fn cancel(self) {
|
|
154
|
+
self.upload_handle.abort();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Finalizes the upload and returns the resulting objects. This will wait for all readers
|
|
158
|
+
/// to finish and all slabs to be uploaded before returning. The resulting objects will contain the metadata needed to download the objects.
|
|
159
|
+
///
|
|
160
|
+
/// The caller must pin the resulting objects to the indexer when ready.
|
|
161
|
+
pub async fn finalize(mut self) -> Result<Vec<Object>, UploadError> {
|
|
162
|
+
let _ = self.writer.shutdown().await; // ignore error
|
|
163
|
+
let uploaded_slabs = self.upload_handle.await??;
|
|
164
|
+
self.objects
|
|
165
|
+
.into_iter()
|
|
166
|
+
.map(|upload| {
|
|
167
|
+
let mut object = upload.object;
|
|
168
|
+
let slabs = object.slabs_mut();
|
|
169
|
+
let slabs_start = upload.start / self.slab_size;
|
|
170
|
+
let slabs_end = upload.end.div_ceil(self.slab_size);
|
|
171
|
+
let n = slabs_end - slabs_start;
|
|
172
|
+
slabs.extend_from_slice(&uploaded_slabs[slabs_start as usize..slabs_end as usize]);
|
|
173
|
+
|
|
174
|
+
slabs[0].offset = (upload.start % self.slab_size) as u32;
|
|
175
|
+
if slabs.len() > 1 {
|
|
176
|
+
// if spanning multiple slabs, adjust first slab's length
|
|
177
|
+
slabs[0].length = (self.slab_size - slabs[0].offset as u64) as u32;
|
|
178
|
+
}
|
|
179
|
+
let last_slab_index = (n - 1) as usize;
|
|
180
|
+
let last_slab_offset = slabs[last_slab_index].offset as u64;
|
|
181
|
+
slabs[last_slab_index].length =
|
|
182
|
+
(upload.end - ((slabs_end - 1) * self.slab_size) - last_slab_offset) as u32;
|
|
183
|
+
|
|
184
|
+
Ok(object)
|
|
185
|
+
})
|
|
186
|
+
.collect()
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// Adds a new object to the upload. The data will be read until EOF and packed into
|
|
190
|
+
/// the upload. The resulting object will contain the metadata needed to download the object. The caller
|
|
191
|
+
/// must call [finalize](Self::finalize) to get the resulting objects after all objects have been added.
|
|
192
|
+
pub async fn add<R: AsyncReadExt + Unpin>(&mut self, r: R) -> io::Result<u64> {
|
|
193
|
+
if self.upload_handle.is_finished() {
|
|
194
|
+
// should only happen if the upload errored; callers can get the error by calling finalize
|
|
195
|
+
return Err(io::Error::other("cannot add object to finalized upload"));
|
|
196
|
+
}
|
|
197
|
+
let object = Object::default();
|
|
198
|
+
let mut r = object.reader(r, 0);
|
|
199
|
+
let object_length = copy(&mut r, &mut self.writer).await?;
|
|
200
|
+
let start = self.length;
|
|
201
|
+
let end = start + object_length;
|
|
202
|
+
self.objects.push(ObjectUpload { start, end, object });
|
|
203
|
+
self.length += object_length;
|
|
204
|
+
Ok(object_length)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#[derive(Clone)]
|
|
209
|
+
pub(crate) struct Uploader {
|
|
210
|
+
app_key: Arc<PrivateKey>,
|
|
211
|
+
hosts: Hosts,
|
|
212
|
+
transport: Arc<dyn RHP4Client>,
|
|
213
|
+
#[cfg(target_arch = "wasm32")]
|
|
214
|
+
default_max_inflight: usize,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
impl Uploader {
|
|
218
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
219
|
+
pub fn new(hosts: Hosts, transport: Arc<dyn RHP4Client>, app_key: Arc<PrivateKey>) -> Self {
|
|
220
|
+
Uploader {
|
|
221
|
+
app_key,
|
|
222
|
+
hosts,
|
|
223
|
+
transport,
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#[cfg(target_arch = "wasm32")]
|
|
228
|
+
pub fn new(
|
|
229
|
+
hosts: Hosts,
|
|
230
|
+
transport: Arc<dyn RHP4Client>,
|
|
231
|
+
app_key: Arc<PrivateKey>,
|
|
232
|
+
default_max_inflight: usize,
|
|
233
|
+
) -> Self {
|
|
234
|
+
Uploader {
|
|
235
|
+
app_key,
|
|
236
|
+
hosts,
|
|
237
|
+
transport,
|
|
238
|
+
default_max_inflight,
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/// Returns default upload options with the configured max_inflight.
|
|
243
|
+
#[cfg(target_arch = "wasm32")]
|
|
244
|
+
pub fn default_options(&self) -> UploadOptions {
|
|
245
|
+
UploadOptions {
|
|
246
|
+
max_inflight: self.default_max_inflight,
|
|
247
|
+
..Default::default()
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async fn upload_shard(
|
|
252
|
+
transport: Arc<dyn RHP4Client>,
|
|
253
|
+
hosts: HostQueue,
|
|
254
|
+
host_key: PublicKey,
|
|
255
|
+
account_key: Arc<PrivateKey>,
|
|
256
|
+
data: Bytes,
|
|
257
|
+
write_timeout: Duration,
|
|
258
|
+
) -> Result<Sector, UploadError> {
|
|
259
|
+
let now = Instant::now();
|
|
260
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
261
|
+
let root = timeout(
|
|
262
|
+
write_timeout,
|
|
263
|
+
transport.write_sector(host_key, &account_key, data),
|
|
264
|
+
)
|
|
265
|
+
.await
|
|
266
|
+
.inspect_err(|e| {
|
|
267
|
+
debug!(
|
|
268
|
+
"upload to host {host_key} timed out after {:?} {e}",
|
|
269
|
+
now.elapsed()
|
|
270
|
+
);
|
|
271
|
+
let _ = hosts.retry(host_key);
|
|
272
|
+
})?
|
|
273
|
+
.inspect_err(|e| {
|
|
274
|
+
debug!(
|
|
275
|
+
"upload to host {host_key} failed after {:?} {e}",
|
|
276
|
+
now.elapsed()
|
|
277
|
+
);
|
|
278
|
+
let _ = hosts.retry(host_key);
|
|
279
|
+
})?;
|
|
280
|
+
#[cfg(target_arch = "wasm32")]
|
|
281
|
+
let root = tokio::select! {
|
|
282
|
+
result = transport.write_sector(host_key, &account_key, data) => {
|
|
283
|
+
result.inspect_err(|e| {
|
|
284
|
+
debug!(
|
|
285
|
+
"upload to host {host_key} failed after {:?} {e}",
|
|
286
|
+
now.elapsed()
|
|
287
|
+
);
|
|
288
|
+
let _ = hosts.retry(host_key);
|
|
289
|
+
})?
|
|
290
|
+
}
|
|
291
|
+
_ = sleep(write_timeout) => {
|
|
292
|
+
debug!(
|
|
293
|
+
"upload to host {host_key} timed out after {:?}",
|
|
294
|
+
now.elapsed()
|
|
295
|
+
);
|
|
296
|
+
let _ = hosts.retry(host_key);
|
|
297
|
+
return Err(UploadError::Rhp4(crate::rhp4::Error::Transport(
|
|
298
|
+
format!("write_sector to {host_key} timed out after {write_timeout:?}")
|
|
299
|
+
)));
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
Ok(Sector { root, host_key })
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
fn upload_timeout(attempts: usize) -> Duration {
|
|
306
|
+
Duration::from_secs((15 + (attempts as u64 * 2)).min(120))
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#[allow(clippy::too_many_arguments)]
|
|
310
|
+
async fn upload_slab_shard(
|
|
311
|
+
permit: OwnedSemaphorePermit,
|
|
312
|
+
transport: Arc<dyn RHP4Client>,
|
|
313
|
+
hosts: HostQueue,
|
|
314
|
+
account_key: Arc<PrivateKey>,
|
|
315
|
+
data: Bytes,
|
|
316
|
+
slab_index: usize,
|
|
317
|
+
shard_index: usize,
|
|
318
|
+
progress_tx: Option<mpsc::UnboundedSender<()>>,
|
|
319
|
+
initial_host: (PublicKey, usize),
|
|
320
|
+
) -> Result<(usize, Sector), UploadError> {
|
|
321
|
+
let (host_key, attempts) = initial_host;
|
|
322
|
+
let mut write_timeout = Self::upload_timeout(attempts); // mutable so that it can be adjusted on retries
|
|
323
|
+
let mut tasks = JoinSet::new();
|
|
324
|
+
join_set_spawn!(tasks, Self::upload_shard(
|
|
325
|
+
transport.clone(),
|
|
326
|
+
hosts.clone(),
|
|
327
|
+
host_key,
|
|
328
|
+
account_key.clone(),
|
|
329
|
+
data.clone(),
|
|
330
|
+
write_timeout,
|
|
331
|
+
));
|
|
332
|
+
let semaphore = permit.semaphore();
|
|
333
|
+
let mut total_failures: usize = 0;
|
|
334
|
+
const MAX_TOTAL_FAILURES: usize = 30; // Give up after 30 failed attempts total
|
|
335
|
+
|
|
336
|
+
loop {
|
|
337
|
+
let active = tasks.len();
|
|
338
|
+
let hosts = hosts.clone();
|
|
339
|
+
tokio::select! {
|
|
340
|
+
biased;
|
|
341
|
+
Some(res) = tasks.join_next() => {
|
|
342
|
+
match res.unwrap() {
|
|
343
|
+
Ok(sector) => {
|
|
344
|
+
if let Some(progress_tx) = progress_tx {
|
|
345
|
+
let _ = progress_tx.send(());
|
|
346
|
+
}
|
|
347
|
+
return Ok((shard_index, sector));
|
|
348
|
+
}
|
|
349
|
+
Err(e) => {
|
|
350
|
+
total_failures += 1;
|
|
351
|
+
debug!("slab {slab_index} shard {shard_index} upload failed ({total_failures}/{MAX_TOTAL_FAILURES}): {e:?}");
|
|
352
|
+
|
|
353
|
+
if total_failures >= MAX_TOTAL_FAILURES {
|
|
354
|
+
return Err(UploadError::Rhp4(crate::rhp4::Error::Transport(
|
|
355
|
+
format!("failed to upload shard after {MAX_TOTAL_FAILURES} attempts")
|
|
356
|
+
)));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if tasks.is_empty() {
|
|
360
|
+
let (host_key, attempts) = hosts.pop_front()?;
|
|
361
|
+
write_timeout = Self::upload_timeout(attempts);
|
|
362
|
+
join_set_spawn!(tasks, Self::upload_shard(transport.clone(), hosts.clone(), host_key, account_key.clone(), data.clone(), write_timeout));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
_ = sleep(Duration::from_secs(active as u64)) => {
|
|
368
|
+
if let Ok(racer) = semaphore.clone().try_acquire_owned() {
|
|
369
|
+
// only race if there's an empty slot
|
|
370
|
+
let transport = transport.clone();
|
|
371
|
+
let data = data.clone();
|
|
372
|
+
let account_key = account_key.clone();
|
|
373
|
+
join_set_spawn!(tasks, async move {
|
|
374
|
+
let _racer = racer; // hold the permit until the task completes
|
|
375
|
+
debug!("slab {slab_index} shard {shard_index} racing slow host");
|
|
376
|
+
let (host_key, attempts) = hosts.pop_front()?;
|
|
377
|
+
let write_timeout = Self::upload_timeout(attempts);
|
|
378
|
+
Self::upload_shard(transport.clone(), hosts.clone(), host_key, account_key, data, write_timeout).await
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async fn upload_slabs<R: AsyncReadExt + Unpin + Send + 'static>(
|
|
387
|
+
transport: Arc<dyn RHP4Client>,
|
|
388
|
+
hosts: Hosts,
|
|
389
|
+
app_key: Arc<PrivateKey>,
|
|
390
|
+
r: R,
|
|
391
|
+
options: UploadOptions,
|
|
392
|
+
) -> Result<Vec<Slab>, UploadError> {
|
|
393
|
+
if options.data_shards == 0 {
|
|
394
|
+
return Err(UploadError::InvalidOptions(
|
|
395
|
+
"data_shards must be greater than 0".to_string(),
|
|
396
|
+
));
|
|
397
|
+
} else if options.parity_shards == 0 {
|
|
398
|
+
return Err(UploadError::InvalidOptions(
|
|
399
|
+
"parity_shards must be greater than 0".to_string(),
|
|
400
|
+
));
|
|
401
|
+
} else if options.max_inflight == 0 {
|
|
402
|
+
return Err(UploadError::InvalidOptions(
|
|
403
|
+
"max_inflight must be greater than 0".to_string(),
|
|
404
|
+
));
|
|
405
|
+
}
|
|
406
|
+
let data_shards = options.data_shards as usize;
|
|
407
|
+
let parity_shards = options.parity_shards as usize;
|
|
408
|
+
let total_shards = data_shards + parity_shards;
|
|
409
|
+
|
|
410
|
+
// fail fast if there aren't enough hosts before doing any encoding
|
|
411
|
+
if hosts.available_for_upload() < total_shards {
|
|
412
|
+
return Err(QueueError::InsufficientHosts.into());
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// hard cap all shard uploads including races
|
|
416
|
+
let shard_sema = Arc::new(Semaphore::new(options.max_inflight));
|
|
417
|
+
// cap number of "active" slabs to limit memory usage.
|
|
418
|
+
let slab_sema = Arc::new(Semaphore::new(
|
|
419
|
+
options
|
|
420
|
+
.max_inflight
|
|
421
|
+
.div_ceil(total_shards)
|
|
422
|
+
.saturating_add(1),
|
|
423
|
+
));
|
|
424
|
+
|
|
425
|
+
// use a buffered reader since the erasure coder reads 64 bytes at a time.
|
|
426
|
+
let mut r = BufReader::new(r);
|
|
427
|
+
let mut slab_upload_tasks = JoinSet::new();
|
|
428
|
+
let rs = Arc::new(ErasureCoder::new(data_shards, parity_shards).unwrap());
|
|
429
|
+
let mut slab_index: usize = 0;
|
|
430
|
+
debug!("upload_slabs: starting upload ({data_shards} data + {parity_shards} parity shards, max_inflight={})", options.max_inflight);
|
|
431
|
+
loop {
|
|
432
|
+
let slab_permit = slab_sema.clone().acquire_owned().await?;
|
|
433
|
+
let mut shards = vec![vec![0u8; SECTOR_SIZE]; total_shards];
|
|
434
|
+
let read_start = Instant::now();
|
|
435
|
+
let length =
|
|
436
|
+
ErasureCoder::read_slab_shards(&mut r, options.data_shards as usize, &mut shards)
|
|
437
|
+
.await?;
|
|
438
|
+
debug!("slab {slab_index}: read_slab_shards took {:?} ({length} bytes)", read_start.elapsed());
|
|
439
|
+
if length == 0 {
|
|
440
|
+
break; // EoF
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
let app_key = app_key.clone();
|
|
444
|
+
let hosts = hosts.clone();
|
|
445
|
+
let transport = transport.clone();
|
|
446
|
+
let progress_tx = options.shard_uploaded.clone();
|
|
447
|
+
let rs = rs.clone();
|
|
448
|
+
let shard_sema = shard_sema.clone();
|
|
449
|
+
|
|
450
|
+
join_set_spawn!(slab_upload_tasks, async move {
|
|
451
|
+
let _slab_guard = slab_permit;
|
|
452
|
+
|
|
453
|
+
// note: it may seem like a good idea to start uploading the data shards
|
|
454
|
+
// while the parity shards are being calculated, but this also forces
|
|
455
|
+
// cloning the rather large shards and ends up being a net performance
|
|
456
|
+
// decrease (~8%).
|
|
457
|
+
//
|
|
458
|
+
// It could probably be resolved by using a pool, but leaving that as a
|
|
459
|
+
// future optimization for now.
|
|
460
|
+
let encode_start = Instant::now();
|
|
461
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
462
|
+
let shards = spawn_blocking(move || -> erasure_coding::Result<Vec<Vec<u8>>> {
|
|
463
|
+
rs.encode_shards(&mut shards)?;
|
|
464
|
+
Ok(shards)
|
|
465
|
+
})
|
|
466
|
+
.await??;
|
|
467
|
+
#[cfg(target_arch = "wasm32")]
|
|
468
|
+
let shards = {
|
|
469
|
+
rs.encode_shards(&mut shards)?;
|
|
470
|
+
shards
|
|
471
|
+
};
|
|
472
|
+
debug!("slab {slab_index}: erasure encode took {:?}", encode_start.elapsed());
|
|
473
|
+
|
|
474
|
+
// generate a unique encryption key for the slab
|
|
475
|
+
let slab_key: EncryptionKey = rand::random::<[u8; 32]>().into();
|
|
476
|
+
|
|
477
|
+
let host_queue = hosts.upload_queue();
|
|
478
|
+
// reserve one host per shard upfront to guarantee each shard has at least one host
|
|
479
|
+
let reserved_hosts = host_queue.pop_n(shards.len())?;
|
|
480
|
+
let owned_slab_key = Arc::new(slab_key.clone());
|
|
481
|
+
let start_time = Instant::now();
|
|
482
|
+
let mut shard_upload_tasks = JoinSet::new();
|
|
483
|
+
for (shard_index, mut shard) in shards.into_iter().enumerate() {
|
|
484
|
+
let app_key = app_key.clone();
|
|
485
|
+
let owned_slab_key = owned_slab_key.clone();
|
|
486
|
+
let permit = shard_sema.clone().acquire_owned().await?;
|
|
487
|
+
let transport = transport.clone();
|
|
488
|
+
let host_queue = host_queue.clone();
|
|
489
|
+
let progress_tx = progress_tx.clone();
|
|
490
|
+
let initial_host = reserved_hosts[shard_index];
|
|
491
|
+
// spawn a task to encrypt and upload each shard for this slab.
|
|
492
|
+
join_set_spawn!(shard_upload_tasks, async move {
|
|
493
|
+
let encrypt_start = Instant::now();
|
|
494
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
495
|
+
let shard = spawn_blocking(move || {
|
|
496
|
+
encrypt_shard(&owned_slab_key, shard_index as u8, 0, &mut shard);
|
|
497
|
+
shard
|
|
498
|
+
})
|
|
499
|
+
.await?;
|
|
500
|
+
#[cfg(target_arch = "wasm32")]
|
|
501
|
+
let shard = {
|
|
502
|
+
encrypt_shard(&owned_slab_key, shard_index as u8, 0, &mut shard);
|
|
503
|
+
shard
|
|
504
|
+
};
|
|
505
|
+
debug!("slab {slab_index} shard {shard_index}: encrypt took {:?}", encrypt_start.elapsed());
|
|
506
|
+
Self::upload_slab_shard(
|
|
507
|
+
permit,
|
|
508
|
+
transport,
|
|
509
|
+
host_queue,
|
|
510
|
+
app_key,
|
|
511
|
+
shard.into(),
|
|
512
|
+
slab_index,
|
|
513
|
+
shard_index,
|
|
514
|
+
progress_tx,
|
|
515
|
+
initial_host,
|
|
516
|
+
)
|
|
517
|
+
.await
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
let mut slab_sectors = vec![None; data_shards + parity_shards];
|
|
522
|
+
let mut remaining_shards = data_shards + parity_shards;
|
|
523
|
+
while let Some(res) = shard_upload_tasks.join_next().await {
|
|
524
|
+
match res {
|
|
525
|
+
Ok(Ok((shard_index, sector))) => {
|
|
526
|
+
slab_sectors[shard_index] = Some(sector);
|
|
527
|
+
remaining_shards -= 1;
|
|
528
|
+
debug!("slab {slab_index} shard {shard_index} uploaded ({remaining_shards} remaining)");
|
|
529
|
+
},
|
|
530
|
+
Ok(Err(e)) => {
|
|
531
|
+
return Err(e);
|
|
532
|
+
}
|
|
533
|
+
Err(e) => {
|
|
534
|
+
return Err(e.into());
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
debug!("slab {slab_index} uploaded in {:?}", Instant::now().duration_since(start_time));
|
|
539
|
+
Ok((slab_index, Slab {
|
|
540
|
+
sectors: slab_sectors.into_iter().map(|s| s.unwrap()).collect(),
|
|
541
|
+
encryption_key: slab_key,
|
|
542
|
+
offset: 0,
|
|
543
|
+
length: length as u32,
|
|
544
|
+
min_shards: options.data_shards,
|
|
545
|
+
}))
|
|
546
|
+
});
|
|
547
|
+
slab_index += 1;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
let num_slabs = slab_upload_tasks.len();
|
|
551
|
+
let mut slabs: Vec<Option<Slab>> = vec![None; num_slabs];
|
|
552
|
+
while let Some(res) = slab_upload_tasks.join_next().await {
|
|
553
|
+
match res {
|
|
554
|
+
Ok(Ok((slab_index, slab))) => {
|
|
555
|
+
slabs[slab_index] = Some(slab);
|
|
556
|
+
}
|
|
557
|
+
Ok(Err(e)) => return Err(e),
|
|
558
|
+
Err(e) => return Err(e.into()),
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
let slabs = slabs.into_iter().map(|s| s.unwrap()).collect();
|
|
562
|
+
Ok(slabs)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/// Reads until EOF and uploads all slabs.
|
|
566
|
+
/// The data will be erasure coded, encrypted,
|
|
567
|
+
/// and uploaded using the uploader's parameters.
|
|
568
|
+
///
|
|
569
|
+
/// # Arguments
|
|
570
|
+
/// * `r` - The reader to read the data from. It will be read until EOF.
|
|
571
|
+
/// * `options` - The [UploadOptions] to use for the upload.
|
|
572
|
+
///
|
|
573
|
+
/// # Returns
|
|
574
|
+
/// A new object containing the metadata needed to download the object. The caller
|
|
575
|
+
/// must pin the object to an indexer after uploading.
|
|
576
|
+
pub async fn upload<R: AsyncReadExt + Unpin + Send + 'static>(
|
|
577
|
+
&self,
|
|
578
|
+
r: R,
|
|
579
|
+
options: UploadOptions,
|
|
580
|
+
) -> Result<Object, UploadError> {
|
|
581
|
+
let mut object = Object::default();
|
|
582
|
+
// use a buffered reader since the erasure coder reads 64 bytes at a time.
|
|
583
|
+
let r = object.reader(BufReader::new(r), 0);
|
|
584
|
+
let new_slabs = Self::upload_slabs(
|
|
585
|
+
self.transport.clone(),
|
|
586
|
+
self.hosts.clone(),
|
|
587
|
+
self.app_key.clone(),
|
|
588
|
+
r,
|
|
589
|
+
options,
|
|
590
|
+
)
|
|
591
|
+
.await?;
|
|
592
|
+
let slabs = object.slabs_mut();
|
|
593
|
+
slabs.extend(new_slabs.into_iter());
|
|
594
|
+
Ok(object)
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/// Creates a new packed upload. This allows multiple objects to be packed together
|
|
598
|
+
/// for more efficient uploads. The returned `PackedUpload` can be used to add objects to the upload, and then finalized to get the resulting objects.
|
|
599
|
+
///
|
|
600
|
+
/// # Arguments
|
|
601
|
+
/// * `options` - The [UploadOptions] to use for the upload.
|
|
602
|
+
///
|
|
603
|
+
/// # Returns
|
|
604
|
+
/// A [PackedUpload] that can be used to add objects and finalize the upload.
|
|
605
|
+
pub fn upload_packed(&self, options: UploadOptions) -> PackedUpload {
|
|
606
|
+
let transport = self.transport.clone();
|
|
607
|
+
let hosts = self.hosts.clone();
|
|
608
|
+
let app_key = self.app_key.clone();
|
|
609
|
+
let (reader, writer) = simplex(1024 * 1024);
|
|
610
|
+
PackedUpload {
|
|
611
|
+
slab_size: options.data_shards as u64 * rhp::SECTOR_SIZE as u64,
|
|
612
|
+
length: 0,
|
|
613
|
+
writer,
|
|
614
|
+
objects: Vec::new(),
|
|
615
|
+
upload_handle: AbortOnDropHandle::new({
|
|
616
|
+
#[cfg(not(target_arch = "wasm32"))]
|
|
617
|
+
{ tokio::spawn(async move {
|
|
618
|
+
let slabs = Self::upload_slabs(transport, hosts, app_key, reader, options).await?;
|
|
619
|
+
Ok(slabs)
|
|
620
|
+
}) }
|
|
621
|
+
#[cfg(target_arch = "wasm32")]
|
|
622
|
+
{ tokio::task::spawn_local(async move {
|
|
623
|
+
let slabs = Self::upload_slabs(transport, hosts, app_key, reader, options).await?;
|
|
624
|
+
Ok(slabs)
|
|
625
|
+
}) }
|
|
626
|
+
}),
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
use std::time::Duration;
|
|
2
|
+
|
|
3
|
+
/// A WASM-compatible monotonic instant backed by `js_sys::Date::now()`.
|
|
4
|
+
///
|
|
5
|
+
/// `std::time::Instant` is not available on `wasm32-unknown-unknown`,
|
|
6
|
+
/// so this provides the subset of the API used by the upload / download
|
|
7
|
+
/// modules.
|
|
8
|
+
#[derive(Clone, Copy, Debug)]
|
|
9
|
+
pub struct Instant(f64);
|
|
10
|
+
|
|
11
|
+
impl Instant {
|
|
12
|
+
pub fn now() -> Self {
|
|
13
|
+
Instant(js_sys::Date::now())
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
pub fn elapsed(&self) -> Duration {
|
|
17
|
+
Duration::from_millis((js_sys::Date::now() - self.0).max(0.0) as u64)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pub fn duration_since(&self, earlier: Instant) -> Duration {
|
|
21
|
+
Duration::from_millis((self.0 - earlier.0).max(0.0) as u64)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// WASM-compatible async sleep using `setTimeout`.
|
|
26
|
+
pub async fn sleep(duration: Duration) {
|
|
27
|
+
wasm_bindgen_futures::JsFuture::from(js_sys::Promise::new(&mut |resolve, _| {
|
|
28
|
+
let global = js_sys::global();
|
|
29
|
+
let set_timeout: js_sys::Function =
|
|
30
|
+
js_sys::Reflect::get(&global, &"setTimeout".into())
|
|
31
|
+
.unwrap()
|
|
32
|
+
.into();
|
|
33
|
+
let _ = set_timeout.call2(
|
|
34
|
+
&wasm_bindgen::JsValue::NULL,
|
|
35
|
+
&resolve,
|
|
36
|
+
&wasm_bindgen::JsValue::from_f64(duration.as_millis() as f64),
|
|
37
|
+
);
|
|
38
|
+
}))
|
|
39
|
+
.await
|
|
40
|
+
.unwrap();
|
|
41
|
+
}
|