mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-09 14:48:08 -05:00
fud, geode: chunks are written and fetched from the full file
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -3076,6 +3076,7 @@ dependencies = [
|
|||||||
"signal-hook",
|
"signal-hook",
|
||||||
"signal-hook-async-std",
|
"signal-hook-async-std",
|
||||||
"simplelog",
|
"simplelog",
|
||||||
|
"sled-overlay",
|
||||||
"smol",
|
"smol",
|
||||||
"structopt",
|
"structopt",
|
||||||
"structopt-toml",
|
"structopt-toml",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ homepage = "https://dark.fi"
|
|||||||
repository = "https://codeberg.org/darkrenaissance/darkfi"
|
repository = "https://codeberg.org/darkrenaissance/darkfi"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
darkfi = {path = "../../../", features = ["async-daemonize", "geode", "rpc", "dht"]}
|
darkfi = {path = "../../../", features = ["async-daemonize", "geode", "rpc", "dht", "sled-overlay"]}
|
||||||
darkfi-serial = {version = "0.4.2", features = ["hash"]}
|
darkfi-serial = {version = "0.4.2", features = ["hash"]}
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
@@ -30,6 +30,9 @@ signal-hook = "0.3.17"
|
|||||||
simplelog = "0.12.2"
|
simplelog = "0.12.2"
|
||||||
smol = "2.0.2"
|
smol = "2.0.2"
|
||||||
|
|
||||||
|
# Database
|
||||||
|
sled-overlay = "0.1.8"
|
||||||
|
|
||||||
# Argument parsing
|
# Argument parsing
|
||||||
serde = {version = "1.0.219", features = ["derive"]}
|
serde = {version = "1.0.219", features = ["derive"]}
|
||||||
structopt = "0.3.26"
|
structopt = "0.3.26"
|
||||||
|
|||||||
206
bin/fud/fud/src/event.rs
Normal file
206
bin/fud/fud/src/event.rs
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
/* This file is part of DarkFi (https://dark.fi)
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2025 Dyne.org foundation
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use tinyjson::JsonValue;
|
||||||
|
|
||||||
|
use darkfi::{
|
||||||
|
geode::hash_to_string,
|
||||||
|
rpc::util::{json_map, json_str},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::resource::Resource;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DownloadStarted {
|
||||||
|
pub hash: blake3::Hash,
|
||||||
|
pub resource: Resource,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ChunkDownloadCompleted {
|
||||||
|
pub hash: blake3::Hash,
|
||||||
|
pub chunk_hash: blake3::Hash,
|
||||||
|
pub resource: Resource,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct FileDownloadCompleted {
|
||||||
|
pub hash: blake3::Hash,
|
||||||
|
pub resource: Resource,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DownloadCompleted {
|
||||||
|
pub hash: blake3::Hash,
|
||||||
|
pub resource: Resource,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ResourceUpdated {
|
||||||
|
pub hash: blake3::Hash,
|
||||||
|
pub resource: Resource,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ResourceRemoved {
|
||||||
|
pub hash: blake3::Hash,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ChunkNotFound {
|
||||||
|
pub hash: blake3::Hash,
|
||||||
|
pub chunk_hash: blake3::Hash,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct FileNotFound {
|
||||||
|
pub hash: blake3::Hash,
|
||||||
|
pub resource: Resource,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct MissingChunks {
|
||||||
|
pub hash: blake3::Hash,
|
||||||
|
pub resource: Resource,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DownloadError {
|
||||||
|
pub hash: blake3::Hash,
|
||||||
|
pub error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum FudEvent {
|
||||||
|
DownloadStarted(DownloadStarted),
|
||||||
|
ChunkDownloadCompleted(ChunkDownloadCompleted),
|
||||||
|
FileDownloadCompleted(FileDownloadCompleted),
|
||||||
|
DownloadCompleted(DownloadCompleted),
|
||||||
|
ResourceUpdated(ResourceUpdated),
|
||||||
|
ResourceRemoved(ResourceRemoved),
|
||||||
|
ChunkNotFound(ChunkNotFound),
|
||||||
|
FileNotFound(FileNotFound),
|
||||||
|
MissingChunks(MissingChunks),
|
||||||
|
DownloadError(DownloadError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DownloadStarted> for JsonValue {
|
||||||
|
fn from(info: DownloadStarted) -> JsonValue {
|
||||||
|
json_map([
|
||||||
|
("hash", JsonValue::String(hash_to_string(&info.hash))),
|
||||||
|
("resource", info.resource.into()),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<ChunkDownloadCompleted> for JsonValue {
|
||||||
|
fn from(info: ChunkDownloadCompleted) -> JsonValue {
|
||||||
|
json_map([
|
||||||
|
("hash", JsonValue::String(hash_to_string(&info.hash))),
|
||||||
|
("chunk_hash", JsonValue::String(hash_to_string(&info.chunk_hash))),
|
||||||
|
("resource", info.resource.into()),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<FileDownloadCompleted> for JsonValue {
|
||||||
|
fn from(info: FileDownloadCompleted) -> JsonValue {
|
||||||
|
json_map([
|
||||||
|
("hash", JsonValue::String(hash_to_string(&info.hash))),
|
||||||
|
("resource", info.resource.into()),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<DownloadCompleted> for JsonValue {
|
||||||
|
fn from(info: DownloadCompleted) -> JsonValue {
|
||||||
|
json_map([
|
||||||
|
("hash", JsonValue::String(hash_to_string(&info.hash))),
|
||||||
|
("resource", info.resource.into()),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<ResourceUpdated> for JsonValue {
|
||||||
|
fn from(info: ResourceUpdated) -> JsonValue {
|
||||||
|
json_map([
|
||||||
|
("hash", JsonValue::String(hash_to_string(&info.hash))),
|
||||||
|
("resource", info.resource.into()),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<ResourceRemoved> for JsonValue {
|
||||||
|
fn from(info: ResourceRemoved) -> JsonValue {
|
||||||
|
json_map([("hash", JsonValue::String(hash_to_string(&info.hash)))])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<ChunkNotFound> for JsonValue {
|
||||||
|
fn from(info: ChunkNotFound) -> JsonValue {
|
||||||
|
json_map([
|
||||||
|
("hash", JsonValue::String(hash_to_string(&info.hash))),
|
||||||
|
("chunk_hash", JsonValue::String(hash_to_string(&info.chunk_hash))),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<FileNotFound> for JsonValue {
|
||||||
|
fn from(info: FileNotFound) -> JsonValue {
|
||||||
|
json_map([
|
||||||
|
("hash", JsonValue::String(hash_to_string(&info.hash))),
|
||||||
|
("resource", info.resource.into()),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<MissingChunks> for JsonValue {
|
||||||
|
fn from(info: MissingChunks) -> JsonValue {
|
||||||
|
json_map([
|
||||||
|
("hash", JsonValue::String(hash_to_string(&info.hash))),
|
||||||
|
("resource", info.resource.into()),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<DownloadError> for JsonValue {
|
||||||
|
fn from(info: DownloadError) -> JsonValue {
|
||||||
|
json_map([
|
||||||
|
("hash", JsonValue::String(hash_to_string(&info.hash))),
|
||||||
|
("error", JsonValue::String(info.error)),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<FudEvent> for JsonValue {
|
||||||
|
fn from(event: FudEvent) -> JsonValue {
|
||||||
|
match event {
|
||||||
|
FudEvent::DownloadStarted(info) => {
|
||||||
|
json_map([("event", json_str("download_started")), ("info", info.into())])
|
||||||
|
}
|
||||||
|
FudEvent::ChunkDownloadCompleted(info) => {
|
||||||
|
json_map([("event", json_str("chunk_download_completed")), ("info", info.into())])
|
||||||
|
}
|
||||||
|
FudEvent::FileDownloadCompleted(info) => {
|
||||||
|
json_map([("event", json_str("file_download_completed")), ("info", info.into())])
|
||||||
|
}
|
||||||
|
FudEvent::DownloadCompleted(info) => {
|
||||||
|
json_map([("event", json_str("download_completed")), ("info", info.into())])
|
||||||
|
}
|
||||||
|
FudEvent::ResourceUpdated(info) => {
|
||||||
|
json_map([("event", json_str("resource_updated")), ("info", info.into())])
|
||||||
|
}
|
||||||
|
FudEvent::ResourceRemoved(info) => {
|
||||||
|
json_map([("event", json_str("resource_removed")), ("info", info.into())])
|
||||||
|
}
|
||||||
|
FudEvent::ChunkNotFound(info) => {
|
||||||
|
json_map([("event", json_str("chunk_not_found")), ("info", info.into())])
|
||||||
|
}
|
||||||
|
FudEvent::FileNotFound(info) => {
|
||||||
|
json_map([("event", json_str("file_not_found")), ("info", info.into())])
|
||||||
|
}
|
||||||
|
FudEvent::MissingChunks(info) => {
|
||||||
|
json_map([("event", json_str("missing_chunks")), ("info", info.into())])
|
||||||
|
}
|
||||||
|
FudEvent::DownloadError(info) => {
|
||||||
|
json_map([("event", json_str("download_error")), ("info", info.into())])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ use futures::{future::FutureExt, pin_mut, select};
|
|||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
use num_bigint::BigUint;
|
use num_bigint::BigUint;
|
||||||
use rand::{prelude::IteratorRandom, rngs::OsRng, seq::SliceRandom, RngCore};
|
use rand::{prelude::IteratorRandom, rngs::OsRng, seq::SliceRandom, RngCore};
|
||||||
|
use sled_overlay::sled;
|
||||||
use smol::{
|
use smol::{
|
||||||
channel,
|
channel,
|
||||||
fs::{File, OpenOptions},
|
fs::{File, OpenOptions},
|
||||||
@@ -56,8 +57,8 @@ use darkfi::{
|
|||||||
Error, Result,
|
Error, Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use event::{ChunkDownloadCompleted, ChunkNotFound, FudEvent, ResourceUpdated};
|
||||||
use resource::{Resource, ResourceStatus};
|
use resource::{Resource, ResourceStatus};
|
||||||
use rpc::{ChunkDownloadCompleted, ChunkNotFound, FudEvent};
|
|
||||||
use tasks::FetchReply;
|
use tasks::FetchReply;
|
||||||
|
|
||||||
/// P2P protocols
|
/// P2P protocols
|
||||||
@@ -68,6 +69,7 @@ use proto::{
|
|||||||
FudPingRequest, ProtocolFud,
|
FudPingRequest, ProtocolFud,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod event;
|
||||||
mod resource;
|
mod resource;
|
||||||
mod rpc;
|
mod rpc;
|
||||||
mod tasks;
|
mod tasks;
|
||||||
@@ -101,7 +103,7 @@ struct Args {
|
|||||||
/// Default path to store downloaded files (defaults to <base_dir>/downloads)
|
/// Default path to store downloaded files (defaults to <base_dir>/downloads)
|
||||||
downloads_path: Option<String>,
|
downloads_path: Option<String>,
|
||||||
|
|
||||||
#[structopt(short, long, default_value = "60")]
|
#[structopt(long, default_value = "60")]
|
||||||
/// Chunk transfer timeout in seconds
|
/// Chunk transfer timeout in seconds
|
||||||
chunk_timeout: u64,
|
chunk_timeout: u64,
|
||||||
|
|
||||||
@@ -140,10 +142,13 @@ pub struct Fud {
|
|||||||
/// Resources (current status of all downloads/seeds)
|
/// Resources (current status of all downloads/seeds)
|
||||||
resources: Arc<RwLock<HashMap<blake3::Hash, Resource>>>,
|
resources: Arc<RwLock<HashMap<blake3::Hash, Resource>>>,
|
||||||
|
|
||||||
|
/// Sled tree containing "resource hash -> path on the filesystem"
|
||||||
|
path_tree: sled::Tree,
|
||||||
|
|
||||||
get_tx: channel::Sender<(u16, blake3::Hash, PathBuf, Result<()>)>,
|
get_tx: channel::Sender<(u16, blake3::Hash, PathBuf, Result<()>)>,
|
||||||
get_rx: channel::Receiver<(u16, blake3::Hash, PathBuf, Result<()>)>,
|
get_rx: channel::Receiver<(u16, blake3::Hash, PathBuf, Result<()>)>,
|
||||||
file_fetch_tx: channel::Sender<(Vec<DhtNode>, blake3::Hash, Result<()>)>,
|
file_fetch_tx: channel::Sender<(Vec<DhtNode>, blake3::Hash, PathBuf, Result<()>)>,
|
||||||
file_fetch_rx: channel::Receiver<(Vec<DhtNode>, blake3::Hash, Result<()>)>,
|
file_fetch_rx: channel::Receiver<(Vec<DhtNode>, blake3::Hash, PathBuf, Result<()>)>,
|
||||||
file_fetch_end_tx: channel::Sender<(blake3::Hash, Result<()>)>,
|
file_fetch_end_tx: channel::Sender<(blake3::Hash, Result<()>)>,
|
||||||
file_fetch_end_rx: channel::Receiver<(blake3::Hash, Result<()>)>,
|
file_fetch_end_rx: channel::Receiver<(blake3::Hash, Result<()>)>,
|
||||||
|
|
||||||
@@ -242,13 +247,37 @@ impl Fud {
|
|||||||
/// Skipped if we have no external address.
|
/// Skipped if we have no external address.
|
||||||
async fn init(&self) -> Result<()> {
|
async fn init(&self) -> Result<()> {
|
||||||
info!(target: "fud::init()", "Finding resources...");
|
info!(target: "fud::init()", "Finding resources...");
|
||||||
let hashes = self.geode.list_files().await?;
|
|
||||||
let mut resources_write = self.resources.write().await;
|
let mut resources_write = self.resources.write().await;
|
||||||
for hash in hashes {
|
for result in self.path_tree.iter() {
|
||||||
|
if result.is_err() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse hash
|
||||||
|
let (hash, path) = result.unwrap();
|
||||||
|
let hash_bytes: [u8; 32] = match hash.to_vec().try_into() {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
let hash = blake3::Hash::from_bytes(hash_bytes);
|
||||||
|
|
||||||
|
// Parse path
|
||||||
|
let path_bytes = path.to_vec();
|
||||||
|
let path_str = match std::str::from_utf8(&path_bytes) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
let path: PathBuf = match expand_path(path_str) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add resource
|
||||||
resources_write.insert(
|
resources_write.insert(
|
||||||
hash,
|
hash,
|
||||||
Resource {
|
Resource {
|
||||||
hash,
|
hash,
|
||||||
|
path,
|
||||||
status: ResourceStatus::Incomplete,
|
status: ResourceStatus::Incomplete,
|
||||||
chunks_total: 0,
|
chunks_total: 0,
|
||||||
chunks_downloaded: 0,
|
chunks_downloaded: 0,
|
||||||
@@ -258,7 +287,7 @@ impl Fud {
|
|||||||
drop(resources_write);
|
drop(resources_write);
|
||||||
|
|
||||||
info!(target: "fud::init()", "Verifying resources...");
|
info!(target: "fud::init()", "Verifying resources...");
|
||||||
let resources = self.get_seeding_resources().await?;
|
let resources = self.verify_resources(None).await?;
|
||||||
|
|
||||||
let self_node = self.dht().node().await;
|
let self_node = self.dht().node().await;
|
||||||
|
|
||||||
@@ -281,11 +310,23 @@ impl Fud {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get resource path from hash using the sled db
|
||||||
|
fn hash_to_path(&self, hash: &blake3::Hash) -> Result<Option<PathBuf>> {
|
||||||
|
if let Some(value) = self.path_tree.get(hash.as_bytes())? {
|
||||||
|
let path: PathBuf = expand_path(std::str::from_utf8(&value)?)?;
|
||||||
|
return Ok(Some(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
/// Verify if resources are complete and uncorrupted.
|
/// Verify if resources are complete and uncorrupted.
|
||||||
/// If a resource is incomplete or corrupted, its status is changed to Incomplete.
|
/// If a resource is incomplete or corrupted, its status is changed to Incomplete.
|
||||||
/// If a resource is complete, its status is changed to Seeding.
|
/// If a resource is complete, its status is changed to Seeding.
|
||||||
|
/// Takes an optional list of hashes.
|
||||||
|
/// If no hash is given (None), it verifies all resources.
|
||||||
/// Returns the list of verified and uncorrupted/complete seeding resources.
|
/// Returns the list of verified and uncorrupted/complete seeding resources.
|
||||||
async fn get_seeding_resources(&self) -> Result<Vec<Resource>> {
|
async fn verify_resources(&self, hashes: Option<Vec<blake3::Hash>>) -> Result<Vec<Resource>> {
|
||||||
let mut resources_write = self.resources.write().await;
|
let mut resources_write = self.resources.write().await;
|
||||||
|
|
||||||
let update_resource =
|
let update_resource =
|
||||||
@@ -301,10 +342,23 @@ impl Fud {
|
|||||||
Some(chunked_file) => chunked_file.local_chunks() as u64,
|
Some(chunked_file) => chunked_file.local_chunks() as u64,
|
||||||
None => 0,
|
None => 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.event_publisher
|
||||||
|
.notify(FudEvent::ResourceUpdated(ResourceUpdated {
|
||||||
|
hash: resource.hash,
|
||||||
|
resource: resource.clone(),
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut seeding_resources: Vec<Resource> = vec![];
|
let mut seeding_resources: Vec<Resource> = vec![];
|
||||||
for (_, mut resource) in resources_write.iter_mut() {
|
for (_, mut resource) in resources_write.iter_mut() {
|
||||||
|
if let Some(ref hashes_list) = hashes {
|
||||||
|
if !hashes_list.contains(&resource.hash) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match resource.status {
|
match resource.status {
|
||||||
ResourceStatus::Seeding => {}
|
ResourceStatus::Seeding => {}
|
||||||
ResourceStatus::Incomplete => {}
|
ResourceStatus::Incomplete => {}
|
||||||
@@ -312,7 +366,14 @@ impl Fud {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Make sure the resource is not corrupted or incomplete
|
// Make sure the resource is not corrupted or incomplete
|
||||||
let chunked_file = match self.geode.get(&resource.hash).await {
|
let resource_path = match self.hash_to_path(&resource.hash) {
|
||||||
|
Ok(Some(v)) => v,
|
||||||
|
Ok(None) | Err(_) => {
|
||||||
|
update_resource(&mut resource, ResourceStatus::Incomplete, None).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let chunked_file = match self.geode.get(&resource.hash, &resource_path).await {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
update_resource(&mut resource, ResourceStatus::Incomplete, None).await;
|
update_resource(&mut resource, ResourceStatus::Incomplete, None).await;
|
||||||
@@ -388,6 +449,7 @@ impl Fud {
|
|||||||
/// Fetch chunks for a file from `seeders`
|
/// Fetch chunks for a file from `seeders`
|
||||||
async fn fetch_chunks(
|
async fn fetch_chunks(
|
||||||
&self,
|
&self,
|
||||||
|
file_path: &PathBuf,
|
||||||
file_hash: &blake3::Hash,
|
file_hash: &blake3::Hash,
|
||||||
chunk_hashes: &HashSet<blake3::Hash>,
|
chunk_hashes: &HashSet<blake3::Hash>,
|
||||||
seeders: &HashSet<DhtRouterItem>,
|
seeders: &HashSet<DhtRouterItem>,
|
||||||
@@ -407,6 +469,7 @@ impl Fud {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let mut chunks_to_query = remaining_chunks.clone();
|
||||||
info!("Requesting chunks from seeder {}", hash_to_string(&seeder.node.id));
|
info!("Requesting chunks from seeder {}", hash_to_string(&seeder.node.id));
|
||||||
loop {
|
loop {
|
||||||
let msg_subsystem = channel.message_subsystem();
|
let msg_subsystem = channel.message_subsystem();
|
||||||
@@ -415,8 +478,6 @@ impl Fud {
|
|||||||
let msg_subscriber_chunk = channel.subscribe_msg::<FudChunkReply>().await.unwrap();
|
let msg_subscriber_chunk = channel.subscribe_msg::<FudChunkReply>().await.unwrap();
|
||||||
let msg_subscriber_notfound = channel.subscribe_msg::<FudNotFound>().await.unwrap();
|
let msg_subscriber_notfound = channel.subscribe_msg::<FudNotFound>().await.unwrap();
|
||||||
|
|
||||||
let mut chunks_to_query = remaining_chunks.clone();
|
|
||||||
|
|
||||||
// Select a chunk to request
|
// Select a chunk to request
|
||||||
let mut chunk_hash: Option<blake3::Hash> = None;
|
let mut chunk_hash: Option<blake3::Hash> = None;
|
||||||
if let Some(&random_chunk) = chunks_to_query.iter().choose(&mut OsRng) {
|
if let Some(&random_chunk) = chunks_to_query.iter().choose(&mut OsRng) {
|
||||||
@@ -428,8 +489,10 @@ impl Fud {
|
|||||||
break; // Switch to another seeder
|
break; // Switch to another seeder
|
||||||
}
|
}
|
||||||
let chunk_hash = chunk_hash.unwrap();
|
let chunk_hash = chunk_hash.unwrap();
|
||||||
|
chunks_to_query.remove(&chunk_hash);
|
||||||
|
|
||||||
let send_res = channel.send(&FudFindRequest { key: chunk_hash }).await;
|
let send_res =
|
||||||
|
channel.send(&FudFindRequest { info: Some(*file_hash), key: chunk_hash }).await;
|
||||||
if let Err(e) = send_res {
|
if let Err(e) = send_res {
|
||||||
warn!(target: "fud::fetch_chunks()", "Error while sending FudFindRequest: {}", e);
|
warn!(target: "fud::fetch_chunks()", "Error while sending FudFindRequest: {}", e);
|
||||||
break; // Switch to another seeder
|
break; // Switch to another seeder
|
||||||
@@ -449,10 +512,9 @@ impl Fud {
|
|||||||
warn!(target: "fud::fetch_chunks()", "Error waiting for chunk reply: {}", e);
|
warn!(target: "fud::fetch_chunks()", "Error waiting for chunk reply: {}", e);
|
||||||
break; // Switch to another seeder
|
break; // Switch to another seeder
|
||||||
}
|
}
|
||||||
chunks_to_query.remove(&chunk_hash);
|
|
||||||
let reply = chunk_reply.unwrap();
|
let reply = chunk_reply.unwrap();
|
||||||
|
|
||||||
match self.geode.insert_chunk(&reply.chunk).await {
|
match self.geode.write_chunk(file_hash, file_path, &reply.chunk).await {
|
||||||
Ok(inserted_hash) => {
|
Ok(inserted_hash) => {
|
||||||
if inserted_hash != chunk_hash {
|
if inserted_hash != chunk_hash {
|
||||||
warn!("Received chunk does not match requested chunk");
|
warn!("Received chunk does not match requested chunk");
|
||||||
@@ -461,7 +523,7 @@ impl Fud {
|
|||||||
continue; // Skip to next chunk, will retry this chunk later
|
continue; // Skip to next chunk, will retry this chunk later
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upade resource `chunks_downloaded`
|
// Update resource `chunks_downloaded`
|
||||||
let mut resources_write = self.resources.write().await;
|
let mut resources_write = self.resources.write().await;
|
||||||
let resource = match resources_write.get_mut(file_hash) {
|
let resource = match resources_write.get_mut(file_hash) {
|
||||||
Some(resource) => {
|
Some(resource) => {
|
||||||
@@ -476,7 +538,7 @@ impl Fud {
|
|||||||
info!(target: "fud::fetch_chunks()", "Received chunk {} from seeder {}", hash_to_string(&chunk_hash), hash_to_string(&seeder.node.id));
|
info!(target: "fud::fetch_chunks()", "Received chunk {} from seeder {}", hash_to_string(&chunk_hash), hash_to_string(&seeder.node.id));
|
||||||
self.event_publisher
|
self.event_publisher
|
||||||
.notify(FudEvent::ChunkDownloadCompleted(ChunkDownloadCompleted {
|
.notify(FudEvent::ChunkDownloadCompleted(ChunkDownloadCompleted {
|
||||||
file_hash: *file_hash,
|
hash: *file_hash,
|
||||||
chunk_hash,
|
chunk_hash,
|
||||||
resource,
|
resource,
|
||||||
}))
|
}))
|
||||||
@@ -498,11 +560,10 @@ impl Fud {
|
|||||||
info!(target: "fud::fetch_chunks()", "Received NOTFOUND {} from seeder {}", hash_to_string(&chunk_hash), hash_to_string(&seeder.node.id));
|
info!(target: "fud::fetch_chunks()", "Received NOTFOUND {} from seeder {}", hash_to_string(&chunk_hash), hash_to_string(&seeder.node.id));
|
||||||
self.event_publisher
|
self.event_publisher
|
||||||
.notify(FudEvent::ChunkNotFound(ChunkNotFound {
|
.notify(FudEvent::ChunkNotFound(ChunkNotFound {
|
||||||
file_hash: *file_hash,
|
hash: *file_hash,
|
||||||
chunk_hash,
|
chunk_hash,
|
||||||
}))
|
}))
|
||||||
.await;
|
.await;
|
||||||
chunks_to_query.remove(&chunk_hash);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -593,7 +654,8 @@ impl Fud {
|
|||||||
let msg_subscriber_notfound =
|
let msg_subscriber_notfound =
|
||||||
channel.subscribe_msg::<FudNotFound>().await.unwrap();
|
channel.subscribe_msg::<FudNotFound>().await.unwrap();
|
||||||
|
|
||||||
let send_res = channel.send(&FudFindRequest { key: file_hash }).await;
|
let send_res =
|
||||||
|
channel.send(&FudFindRequest { info: None, key: file_hash }).await;
|
||||||
if let Err(e) = send_res {
|
if let Err(e) = send_res {
|
||||||
warn!(target: "fud::fetch_file_metadata()", "Error while sending FudFindRequest: {}", e);
|
warn!(target: "fud::fetch_file_metadata()", "Error while sending FudFindRequest: {}", e);
|
||||||
msg_subscriber_chunk.unsubscribe().await;
|
msg_subscriber_chunk.unsubscribe().await;
|
||||||
@@ -697,8 +759,12 @@ async fn realmain(args: Args, ex: Arc<Executor<'static>>) -> Result<()> {
|
|||||||
// Hashmap used for routing
|
// Hashmap used for routing
|
||||||
let seeders_router = Arc::new(RwLock::new(HashMap::new()));
|
let seeders_router = Arc::new(RwLock::new(HashMap::new()));
|
||||||
|
|
||||||
|
// Sled database init
|
||||||
|
info!("Instantiating database");
|
||||||
|
let sled_db = sled::open(basedir.join("db"))?;
|
||||||
|
|
||||||
info!("Instantiating Geode instance");
|
info!("Instantiating Geode instance");
|
||||||
let geode = Geode::new(&basedir).await?;
|
let geode = Geode::new(&basedir /*, sled_db, "geode"*/).await?;
|
||||||
|
|
||||||
info!("Instantiating P2P network");
|
info!("Instantiating P2P network");
|
||||||
let net_settings: NetSettings = args.net.into();
|
let net_settings: NetSettings = args.net.into();
|
||||||
@@ -771,6 +837,7 @@ async fn realmain(args: Args, ex: Arc<Executor<'static>>) -> Result<()> {
|
|||||||
downloads_path,
|
downloads_path,
|
||||||
chunk_timeout: args.chunk_timeout,
|
chunk_timeout: args.chunk_timeout,
|
||||||
dht: dht.clone(),
|
dht: dht.clone(),
|
||||||
|
path_tree: sled_db.open_tree("path")?,
|
||||||
resources: Arc::new(RwLock::new(HashMap::new())),
|
resources: Arc::new(RwLock::new(HashMap::new())),
|
||||||
get_tx,
|
get_tx,
|
||||||
get_rx,
|
get_rx,
|
||||||
|
|||||||
@@ -18,12 +18,12 @@
|
|||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
use smol::{fs::File, Executor};
|
use smol::Executor;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use darkfi::{
|
use darkfi::{
|
||||||
dht::{DhtHandler, DhtNode, DhtRouterItem},
|
dht::{DhtHandler, DhtNode, DhtRouterItem},
|
||||||
geode::{hash_to_string, read_until_filled, MAX_CHUNK_SIZE},
|
geode::hash_to_string,
|
||||||
impl_p2p_message,
|
impl_p2p_message,
|
||||||
net::{
|
net::{
|
||||||
metering::{MeteringConfiguration, DEFAULT_METERING_CONFIGURATION},
|
metering::{MeteringConfiguration, DEFAULT_METERING_CONFIGURATION},
|
||||||
@@ -79,6 +79,7 @@ impl_p2p_message!(FudPingReply, "FudPingReply", 0, 0, DEFAULT_METERING_CONFIGURA
|
|||||||
/// Message representing a find file/chunk request from the network
|
/// Message representing a find file/chunk request from the network
|
||||||
#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
|
#[derive(Debug, Clone, SerialEncodable, SerialDecodable)]
|
||||||
pub struct FudFindRequest {
|
pub struct FudFindRequest {
|
||||||
|
pub info: Option<blake3::Hash>,
|
||||||
pub key: blake3::Hash,
|
pub key: blake3::Hash,
|
||||||
}
|
}
|
||||||
impl_p2p_message!(FudFindRequest, "FudFindRequest", 0, 0, DEFAULT_METERING_CONFIGURATION);
|
impl_p2p_message!(FudFindRequest, "FudFindRequest", 0, 0, DEFAULT_METERING_CONFIGURATION);
|
||||||
@@ -202,63 +203,87 @@ impl ProtocolFud {
|
|||||||
self.fud.update_node(&node).await;
|
self.fud.update_node(&node).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chunk
|
if self.handle_fud_chunk_request(&request).await {
|
||||||
{
|
continue;
|
||||||
let chunk_res = self.fud.geode.get_chunk(&request.key).await;
|
|
||||||
|
|
||||||
// TODO: Run geode GC
|
|
||||||
|
|
||||||
if let Ok(chunk_path) = chunk_res {
|
|
||||||
let mut buf = vec![0u8; MAX_CHUNK_SIZE];
|
|
||||||
let mut chunk_fd = File::open(&chunk_path).await.unwrap();
|
|
||||||
let bytes_read = read_until_filled(&mut chunk_fd, &mut buf).await.unwrap();
|
|
||||||
let chunk_slice = &buf[..bytes_read];
|
|
||||||
let reply = FudChunkReply { chunk: chunk_slice.to_vec() };
|
|
||||||
info!(target: "fud::ProtocolFud::handle_fud_find_request()", "Sending chunk");
|
|
||||||
let _ = self.channel.send(&reply).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// File
|
if self.handle_fud_file_request(&request).await {
|
||||||
{
|
continue;
|
||||||
let file_res = self.fud.geode.get(&request.key).await;
|
|
||||||
|
|
||||||
// TODO: Run geode GC
|
|
||||||
|
|
||||||
if let Ok(chunked_file) = file_res {
|
|
||||||
// If the file has a single chunk, just reply with the chunk
|
|
||||||
if chunked_file.len() == 1 {
|
|
||||||
let chunk_res =
|
|
||||||
self.fud.geode.get_chunk(&chunked_file.iter().next().unwrap().0).await;
|
|
||||||
if let Ok(chunk_path) = chunk_res {
|
|
||||||
let mut buf = vec![0u8; MAX_CHUNK_SIZE];
|
|
||||||
let mut chunk_fd = File::open(&chunk_path).await.unwrap();
|
|
||||||
let bytes_read =
|
|
||||||
read_until_filled(&mut chunk_fd, &mut buf).await.unwrap();
|
|
||||||
let chunk_slice = &buf[..bytes_read];
|
|
||||||
let reply = FudChunkReply { chunk: chunk_slice.to_vec() };
|
|
||||||
info!(target: "fud::ProtocolFud::handle_fud_find_request()", "Sending chunk (file has a single chunk)");
|
|
||||||
let _ = self.channel.send(&reply).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Otherwise reply with the file metadata
|
|
||||||
let reply = FudFileReply {
|
|
||||||
chunk_hashes: chunked_file.iter().map(|(chunk, _)| *chunk).collect(),
|
|
||||||
};
|
|
||||||
info!(target: "fud::ProtocolFud::handle_fud_find_request()", "Sending file");
|
|
||||||
let _ = self.channel.send(&reply).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Request did not match anything we have
|
||||||
let reply = FudNotFound {};
|
let reply = FudNotFound {};
|
||||||
info!(target: "fud::ProtocolFud::handle_fud_find_request()", "We do not have {}", hash_to_string(&request.key));
|
info!(target: "fud::ProtocolFud::handle_fud_find_request()", "We do not have {}", hash_to_string(&request.key));
|
||||||
let _ = self.channel.send(&reply).await;
|
let _ = self.channel.send(&reply).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If the FudFindRequest matches a chunk we have, handle it.
|
||||||
|
/// Returns true if the chunk was found.
|
||||||
|
async fn handle_fud_chunk_request(&self, request: &FudFindRequest) -> bool {
|
||||||
|
let file_hash = request.info;
|
||||||
|
if file_hash.is_none() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let file_hash = file_hash.unwrap();
|
||||||
|
|
||||||
|
let file_path = self.fud.hash_to_path(&file_hash).ok().flatten();
|
||||||
|
if file_path.is_none() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let file_path = file_path.unwrap();
|
||||||
|
|
||||||
|
let chunk = self.fud.geode.get_chunk(&request.key, &file_hash, &file_path).await;
|
||||||
|
if let Ok(chunk) = chunk {
|
||||||
|
// TODO: Run geode GC
|
||||||
|
let reply = FudChunkReply { chunk };
|
||||||
|
info!(target: "fud::ProtocolFud::handle_fud_find_request()", "Sending chunk");
|
||||||
|
let _ = self.channel.send(&reply).await;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If the FudFindRequest matches a file we have, handle it
|
||||||
|
/// Returns true if the file was found.
|
||||||
|
async fn handle_fud_file_request(&self, request: &FudFindRequest) -> bool {
|
||||||
|
let file_path = self.fud.hash_to_path(&request.key).ok().flatten();
|
||||||
|
if file_path.is_none() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let file_path = file_path.unwrap();
|
||||||
|
|
||||||
|
let chunked_file = self.fud.geode.get(&request.key, &file_path).await.ok();
|
||||||
|
if chunked_file.is_none() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let chunked_file = chunked_file.unwrap();
|
||||||
|
|
||||||
|
// If the file has a single chunk, just reply with the chunk
|
||||||
|
if chunked_file.len() == 1 {
|
||||||
|
let chunk = self
|
||||||
|
.fud
|
||||||
|
.geode
|
||||||
|
.get_chunk(&chunked_file.iter().next().unwrap().0, &request.key, &file_path)
|
||||||
|
.await;
|
||||||
|
if let Ok(chunk) = chunk {
|
||||||
|
// TODO: Run geode GC
|
||||||
|
let reply = FudChunkReply { chunk };
|
||||||
|
info!(target: "fud::ProtocolFud::handle_fud_find_request()", "Sending chunk (file has a single chunk)");
|
||||||
|
let _ = self.channel.send(&reply).await;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise reply with the file metadata
|
||||||
|
let reply =
|
||||||
|
FudFileReply { chunk_hashes: chunked_file.iter().map(|(chunk, _)| *chunk).collect() };
|
||||||
|
info!(target: "fud::ProtocolFud::handle_fud_find_request()", "Sending file");
|
||||||
|
let _ = self.channel.send(&reply).await;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_fud_find_nodes_request(self: Arc<Self>) -> Result<()> {
|
async fn handle_fud_find_nodes_request(self: Arc<Self>) -> Result<()> {
|
||||||
debug!(target: "fud::ProtocolFud::handle_fud_find_nodes_request()", "START");
|
debug!(target: "fud::ProtocolFud::handle_fud_find_nodes_request()", "START");
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
use darkfi::{geode::hash_to_string, rpc::util::json_map};
|
use darkfi::{geode::hash_to_string, rpc::util::json_map};
|
||||||
|
use std::path::PathBuf;
|
||||||
use tinyjson::JsonValue;
|
use tinyjson::JsonValue;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -30,6 +31,7 @@ pub enum ResourceStatus {
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Resource {
|
pub struct Resource {
|
||||||
pub hash: blake3::Hash,
|
pub hash: blake3::Hash,
|
||||||
|
pub path: PathBuf,
|
||||||
pub status: ResourceStatus,
|
pub status: ResourceStatus,
|
||||||
pub chunks_total: u64,
|
pub chunks_total: u64,
|
||||||
pub chunks_downloaded: u64,
|
pub chunks_downloaded: u64,
|
||||||
@@ -39,6 +41,13 @@ impl From<Resource> for JsonValue {
|
|||||||
fn from(rs: Resource) -> JsonValue {
|
fn from(rs: Resource) -> JsonValue {
|
||||||
json_map([
|
json_map([
|
||||||
("hash", JsonValue::String(hash_to_string(&rs.hash))),
|
("hash", JsonValue::String(hash_to_string(&rs.hash))),
|
||||||
|
(
|
||||||
|
"path",
|
||||||
|
JsonValue::String(match rs.path.into_os_string().into_string() {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(_) => "".to_string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"status",
|
"status",
|
||||||
JsonValue::String(
|
JsonValue::String(
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ use darkfi::{
|
|||||||
jsonrpc::{ErrorCode, JsonError, JsonRequest, JsonResponse, JsonResult},
|
jsonrpc::{ErrorCode, JsonError, JsonRequest, JsonResponse, JsonResult},
|
||||||
p2p_method::HandlerP2p,
|
p2p_method::HandlerP2p,
|
||||||
server::RequestHandler,
|
server::RequestHandler,
|
||||||
util::{json_map, json_str},
|
|
||||||
},
|
},
|
||||||
system::StoppableTaskPtr,
|
system::StoppableTaskPtr,
|
||||||
util::path::expand_path,
|
util::path::expand_path,
|
||||||
@@ -43,6 +42,7 @@ use darkfi::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
event::{self, FudEvent},
|
||||||
proto::FudAnnounce,
|
proto::FudAnnounce,
|
||||||
resource::{Resource, ResourceStatus},
|
resource::{Resource, ResourceStatus},
|
||||||
Fud,
|
Fud,
|
||||||
@@ -61,6 +61,7 @@ impl RequestHandler<()> for Fud {
|
|||||||
"list_resources" => self.list_resources(req.id, req.params).await,
|
"list_resources" => self.list_resources(req.id, req.params).await,
|
||||||
"list_buckets" => self.list_buckets(req.id, req.params).await,
|
"list_buckets" => self.list_buckets(req.id, req.params).await,
|
||||||
"list_seeders" => self.list_seeders(req.id, req.params).await,
|
"list_seeders" => self.list_seeders(req.id, req.params).await,
|
||||||
|
"verify" => self.verify(req.id, req.params).await,
|
||||||
|
|
||||||
"dnet.switch" => self.dnet_switch(req.id, req.params).await,
|
"dnet.switch" => self.dnet_switch(req.id, req.params).await,
|
||||||
"dnet.subscribe_events" => self.dnet_subscribe_events(req.id, req.params).await,
|
"dnet.subscribe_events" => self.dnet_subscribe_events(req.id, req.params).await,
|
||||||
@@ -116,7 +117,7 @@ impl Fud {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let (file_hash, _) = match self.geode.insert(fd).await {
|
let (file_hash, chunk_hashes) = match self.geode.insert(fd).await {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let error_str = format!("Failed inserting file {:?} to geode: {}", path, e);
|
let error_str = format!("Failed inserting file {:?} to geode: {}", path, e);
|
||||||
@@ -125,6 +126,29 @@ impl Fud {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add path to the sled db
|
||||||
|
if let Err(e) = self
|
||||||
|
.path_tree
|
||||||
|
.insert(file_hash.as_bytes(), path.to_string_lossy().to_string().as_bytes())
|
||||||
|
{
|
||||||
|
error!(target: "fud::put()", "Failed inserting new file into sled: {}", e);
|
||||||
|
return JsonError::new(ErrorCode::InternalError, None, id).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add resource
|
||||||
|
let mut resources_write = self.resources.write().await;
|
||||||
|
resources_write.insert(
|
||||||
|
file_hash,
|
||||||
|
Resource {
|
||||||
|
hash: file_hash,
|
||||||
|
path,
|
||||||
|
status: ResourceStatus::Seeding,
|
||||||
|
chunks_total: chunk_hashes.len() as u64,
|
||||||
|
chunks_downloaded: chunk_hashes.len() as u64,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
drop(resources_write);
|
||||||
|
|
||||||
// Announce file
|
// Announce file
|
||||||
let fud_announce = FudAnnounce { key: file_hash, seeders: vec![self_node.into()] };
|
let fud_announce = FudAnnounce { key: file_hash, seeders: vec![self_node.into()] };
|
||||||
let _ = self.announce(&file_hash, &fud_announce, self.seeders_router.clone()).await;
|
let _ = self.announce(&file_hash, &fud_announce, self.seeders_router.clone()).await;
|
||||||
@@ -137,7 +161,7 @@ impl Fud {
|
|||||||
// Returns the path where the file will be located once downloaded.
|
// Returns the path where the file will be located once downloaded.
|
||||||
//
|
//
|
||||||
// --> {"jsonrpc": "2.0", "method": "get", "params": ["1211...abfd", "~/myfile.jpg"], "id": 42}
|
// --> {"jsonrpc": "2.0", "method": "get", "params": ["1211...abfd", "~/myfile.jpg"], "id": 42}
|
||||||
// <-- {"jsonrpc": "2.0", "method": "get", "params": "/home/user/myfile.jpg"}
|
// <-- {"jsonrpc": "2.0", "result": "/home/user/myfile.jpg", "id": 42}
|
||||||
async fn get(&self, id: u16, params: JsonValue) -> JsonResult {
|
async fn get(&self, id: u16, params: JsonValue) -> JsonResult {
|
||||||
let params = params.get::<Vec<JsonValue>>().unwrap();
|
let params = params.get::<Vec<JsonValue>>().unwrap();
|
||||||
if params.len() != 2 || !params[0].is_string() || !params[1].is_string() {
|
if params.len() != 2 || !params[0].is_string() || !params[1].is_string() {
|
||||||
@@ -186,7 +210,7 @@ impl Fud {
|
|||||||
// Subscribe to download events.
|
// Subscribe to download events.
|
||||||
//
|
//
|
||||||
// --> {"jsonrpc": "2.0", "method": "get", "params": [], "id": 42}
|
// --> {"jsonrpc": "2.0", "method": "get", "params": [], "id": 42}
|
||||||
// <-- {"jsonrpc": "2.0", "method": "get", "params": `event`}
|
// <-- {"jsonrpc": "2.0", "result": `event`, "id": 42}
|
||||||
async fn subscribe(&self, _id: u16, _params: JsonValue) -> JsonResult {
|
async fn subscribe(&self, _id: u16, _params: JsonValue) -> JsonResult {
|
||||||
self.event_sub.clone().into()
|
self.event_sub.clone().into()
|
||||||
}
|
}
|
||||||
@@ -232,7 +256,7 @@ impl Fud {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RPCAPI:
|
// RPCAPI:
|
||||||
// Returns resources from the database.
|
// Returns resources.
|
||||||
//
|
//
|
||||||
// --> {"jsonrpc": "2.0", "method": "list_buckets", "params": [], "id": 1}
|
// --> {"jsonrpc": "2.0", "method": "list_buckets", "params": [], "id": 1}
|
||||||
// <-- {"jsonrpc": "2.0", "result": [[["abcdef", ["tcp://127.0.0.1:13337"]]]], "id": 1}
|
// <-- {"jsonrpc": "2.0", "result": [[["abcdef", ["tcp://127.0.0.1:13337"]]]], "id": 1}
|
||||||
@@ -326,177 +350,49 @@ impl Fud {
|
|||||||
drop(resources_write);
|
drop(resources_write);
|
||||||
|
|
||||||
self.event_publisher
|
self.event_publisher
|
||||||
.notify(FudEvent::ResourceRemoved(ResourceRemoved { file_hash: hash }))
|
.notify(FudEvent::ResourceRemoved(event::ResourceRemoved { hash }))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
JsonResponse::new(JsonValue::Array(vec![]), id).into()
|
JsonResponse::new(JsonValue::Array(vec![]), id).into()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
// RPCAPI:
|
||||||
pub struct DownloadStarted {
|
// Verifies local files. Takes a list of file hashes as parameters.
|
||||||
pub file_hash: blake3::Hash,
|
// An empty list means all known files.
|
||||||
pub file_path: PathBuf,
|
// Returns the path where the file will be located once downloaded.
|
||||||
pub resource: Resource,
|
//
|
||||||
}
|
// --> {"jsonrpc": "2.0", "method": "verify", "params": ["1211...abfd"], "id": 42}
|
||||||
#[derive(Clone, Debug)]
|
// <-- {"jsonrpc": "2.0", "result": [], "id": 1}
|
||||||
pub struct ChunkDownloadCompleted {
|
async fn verify(&self, id: u16, params: JsonValue) -> JsonResult {
|
||||||
pub file_hash: blake3::Hash,
|
let params = params.get::<Vec<JsonValue>>().unwrap();
|
||||||
pub chunk_hash: blake3::Hash,
|
if !params.iter().all(|param| param.is_string()) {
|
||||||
pub resource: Resource,
|
return JsonError::new(ErrorCode::InvalidParams, None, id).into()
|
||||||
}
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct FileDownloadCompleted {
|
|
||||||
pub file_hash: blake3::Hash,
|
|
||||||
pub resource: Resource,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct DownloadCompleted {
|
|
||||||
pub file_hash: blake3::Hash,
|
|
||||||
pub file_path: PathBuf,
|
|
||||||
pub resource: Resource,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct ResourceRemoved {
|
|
||||||
pub file_hash: blake3::Hash,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct ChunkNotFound {
|
|
||||||
pub file_hash: blake3::Hash,
|
|
||||||
pub chunk_hash: blake3::Hash,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct FileNotFound {
|
|
||||||
pub file_hash: blake3::Hash,
|
|
||||||
pub resource: Resource,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct MissingChunks {
|
|
||||||
pub file_hash: blake3::Hash,
|
|
||||||
pub resource: Resource,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct DownloadError {
|
|
||||||
pub file_hash: blake3::Hash,
|
|
||||||
pub error: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum FudEvent {
|
|
||||||
DownloadStarted(DownloadStarted),
|
|
||||||
ChunkDownloadCompleted(ChunkDownloadCompleted),
|
|
||||||
FileDownloadCompleted(FileDownloadCompleted),
|
|
||||||
DownloadCompleted(DownloadCompleted),
|
|
||||||
ResourceRemoved(ResourceRemoved),
|
|
||||||
ChunkNotFound(ChunkNotFound),
|
|
||||||
FileNotFound(FileNotFound),
|
|
||||||
MissingChunks(MissingChunks),
|
|
||||||
DownloadError(DownloadError),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DownloadStarted> for JsonValue {
|
|
||||||
fn from(info: DownloadStarted) -> JsonValue {
|
|
||||||
json_map([
|
|
||||||
("file_hash", JsonValue::String(hash_to_string(&info.file_hash))),
|
|
||||||
("file_path", JsonValue::String(info.file_path.to_string_lossy().to_string())),
|
|
||||||
("resource", info.resource.into()),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<ChunkDownloadCompleted> for JsonValue {
|
|
||||||
fn from(info: ChunkDownloadCompleted) -> JsonValue {
|
|
||||||
json_map([
|
|
||||||
("file_hash", JsonValue::String(hash_to_string(&info.file_hash))),
|
|
||||||
("chunk_hash", JsonValue::String(hash_to_string(&info.chunk_hash))),
|
|
||||||
("resource", info.resource.into()),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<FileDownloadCompleted> for JsonValue {
|
|
||||||
fn from(info: FileDownloadCompleted) -> JsonValue {
|
|
||||||
json_map([
|
|
||||||
("file_hash", JsonValue::String(hash_to_string(&info.file_hash))),
|
|
||||||
("resource", info.resource.into()),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<DownloadCompleted> for JsonValue {
|
|
||||||
fn from(info: DownloadCompleted) -> JsonValue {
|
|
||||||
json_map([
|
|
||||||
("file_hash", JsonValue::String(hash_to_string(&info.file_hash))),
|
|
||||||
("file_path", JsonValue::String(info.file_path.to_string_lossy().to_string())),
|
|
||||||
("resource", info.resource.into()),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<ResourceRemoved> for JsonValue {
|
|
||||||
fn from(info: ResourceRemoved) -> JsonValue {
|
|
||||||
json_map([("file_hash", JsonValue::String(hash_to_string(&info.file_hash)))])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<ChunkNotFound> for JsonValue {
|
|
||||||
fn from(info: ChunkNotFound) -> JsonValue {
|
|
||||||
json_map([
|
|
||||||
("file_hash", JsonValue::String(hash_to_string(&info.file_hash))),
|
|
||||||
("chunk_hash", JsonValue::String(hash_to_string(&info.chunk_hash))),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<FileNotFound> for JsonValue {
|
|
||||||
fn from(info: FileNotFound) -> JsonValue {
|
|
||||||
json_map([
|
|
||||||
("file_hash", JsonValue::String(hash_to_string(&info.file_hash))),
|
|
||||||
("resource", info.resource.into()),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<MissingChunks> for JsonValue {
|
|
||||||
fn from(info: MissingChunks) -> JsonValue {
|
|
||||||
json_map([
|
|
||||||
("file_hash", JsonValue::String(hash_to_string(&info.file_hash))),
|
|
||||||
("resource", info.resource.into()),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<DownloadError> for JsonValue {
|
|
||||||
fn from(info: DownloadError) -> JsonValue {
|
|
||||||
json_map([
|
|
||||||
("file_hash", JsonValue::String(hash_to_string(&info.file_hash))),
|
|
||||||
("error", JsonValue::String(info.error)),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<FudEvent> for JsonValue {
|
|
||||||
fn from(event: FudEvent) -> JsonValue {
|
|
||||||
match event {
|
|
||||||
FudEvent::DownloadStarted(info) => {
|
|
||||||
json_map([("event", json_str("download_started")), ("info", info.into())])
|
|
||||||
}
|
|
||||||
FudEvent::ChunkDownloadCompleted(info) => {
|
|
||||||
json_map([("event", json_str("chunk_download_completed")), ("info", info.into())])
|
|
||||||
}
|
|
||||||
FudEvent::FileDownloadCompleted(info) => {
|
|
||||||
json_map([("event", json_str("file_download_completed")), ("info", info.into())])
|
|
||||||
}
|
|
||||||
FudEvent::DownloadCompleted(info) => {
|
|
||||||
json_map([("event", json_str("download_completed")), ("info", info.into())])
|
|
||||||
}
|
|
||||||
FudEvent::ResourceRemoved(info) => {
|
|
||||||
json_map([("event", json_str("resource_removed")), ("info", info.into())])
|
|
||||||
}
|
|
||||||
FudEvent::ChunkNotFound(info) => {
|
|
||||||
json_map([("event", json_str("chunk_not_found")), ("info", info.into())])
|
|
||||||
}
|
|
||||||
FudEvent::FileNotFound(info) => {
|
|
||||||
json_map([("event", json_str("file_not_found")), ("info", info.into())])
|
|
||||||
}
|
|
||||||
FudEvent::MissingChunks(info) => {
|
|
||||||
json_map([("event", json_str("missing_chunks")), ("info", info.into())])
|
|
||||||
}
|
|
||||||
FudEvent::DownloadError(info) => {
|
|
||||||
json_map([("event", json_str("download_error")), ("info", info.into())])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
let hashes = if params.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let hashes_str: Vec<String> =
|
||||||
|
params.iter().map(|param| param.get::<String>().unwrap().clone()).collect();
|
||||||
|
let hashes: Result<Vec<blake3::Hash>> = hashes_str
|
||||||
|
.into_iter()
|
||||||
|
.map(|hash_str| {
|
||||||
|
let mut buf = [0u8; 32];
|
||||||
|
bs58::decode(hash_str).onto(&mut buf)?;
|
||||||
|
Ok(blake3::Hash::from_bytes(buf))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if hashes.is_err() {
|
||||||
|
return JsonError::new(ErrorCode::InvalidParams, None, id).into();
|
||||||
|
}
|
||||||
|
Some(hashes.unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = self.verify_resources(hashes).await {
|
||||||
|
error!(target: "fud::verify()", "Could not verify resources: {}", e);
|
||||||
|
return JsonError::new(ErrorCode::InternalError, None, id).into();
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonResponse::new(JsonValue::Array(vec![]), id).into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,10 +400,16 @@ impl Fud {
|
|||||||
/// Handle `get` RPC request
|
/// Handle `get` RPC request
|
||||||
pub async fn handle_get(&self, file_hash: &blake3::Hash, file_path: &PathBuf) -> Result<()> {
|
pub async fn handle_get(&self, file_hash: &blake3::Hash, file_path: &PathBuf) -> Result<()> {
|
||||||
let self_node = self.dht().node().await;
|
let self_node = self.dht().node().await;
|
||||||
|
let mut closest_nodes = vec![];
|
||||||
|
|
||||||
|
// Add path to the sled db
|
||||||
|
self.path_tree
|
||||||
|
.insert(file_hash.as_bytes(), file_path.to_string_lossy().to_string().as_bytes())?;
|
||||||
|
|
||||||
// Add resource to `self.resources`
|
// Add resource to `self.resources`
|
||||||
let resource = Resource {
|
let resource = Resource {
|
||||||
hash: *file_hash,
|
hash: *file_hash,
|
||||||
|
path: file_path.clone(),
|
||||||
status: ResourceStatus::Discovering,
|
status: ResourceStatus::Discovering,
|
||||||
chunks_total: 0,
|
chunks_total: 0,
|
||||||
chunks_downloaded: 0,
|
chunks_downloaded: 0,
|
||||||
@@ -516,28 +418,37 @@ impl Fud {
|
|||||||
resources_write.insert(*file_hash, resource.clone());
|
resources_write.insert(*file_hash, resource.clone());
|
||||||
drop(resources_write);
|
drop(resources_write);
|
||||||
|
|
||||||
|
// Send a DownloadStarted event
|
||||||
self.event_publisher
|
self.event_publisher
|
||||||
.notify(FudEvent::DownloadStarted(DownloadStarted {
|
.notify(FudEvent::DownloadStarted(event::DownloadStarted {
|
||||||
file_hash: *file_hash,
|
hash: *file_hash,
|
||||||
file_path: file_path.clone(),
|
|
||||||
resource,
|
resource,
|
||||||
}))
|
}))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let mut closest_nodes = vec![];
|
// Try to get the chunked file from geode
|
||||||
|
let chunked_file = match self.geode.get(file_hash, file_path).await {
|
||||||
let chunked_file = match self.geode.get(file_hash).await {
|
// We already know the list of chunk hashes for this file
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
|
// The metadata in geode is invalid or corrupted
|
||||||
Err(Error::GeodeNeedsGc) => todo!(),
|
Err(Error::GeodeNeedsGc) => todo!(),
|
||||||
|
// If we could not find the file in geode, get the file metadata from the network
|
||||||
Err(Error::GeodeFileNotFound) => {
|
Err(Error::GeodeFileNotFound) => {
|
||||||
|
// Find nodes close to the file hash
|
||||||
info!(target: "self::get()", "Requested file {} not found in Geode, triggering fetch", hash_to_string(file_hash));
|
info!(target: "self::get()", "Requested file {} not found in Geode, triggering fetch", hash_to_string(file_hash));
|
||||||
closest_nodes = self.lookup_nodes(file_hash).await.unwrap_or_default();
|
closest_nodes = self.lookup_nodes(file_hash).await.unwrap_or_default();
|
||||||
self.file_fetch_tx.send((closest_nodes.clone(), *file_hash, Ok(()))).await.unwrap();
|
|
||||||
|
// Fetch file metadata (list of chunk hashes)
|
||||||
|
self.file_fetch_tx
|
||||||
|
.send((closest_nodes.clone(), *file_hash, file_path.clone(), Ok(())))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
info!(target: "self::get()", "Waiting for background file fetch task...");
|
info!(target: "self::get()", "Waiting for background file fetch task...");
|
||||||
let (i_file_hash, status) = self.file_fetch_end_rx.recv().await.unwrap();
|
let (i_file_hash, status) = self.file_fetch_end_rx.recv().await.unwrap();
|
||||||
match status {
|
match status {
|
||||||
Ok(()) => self.geode.get(&i_file_hash).await.unwrap(),
|
// The file metadata was found and inserted into geode
|
||||||
|
Ok(()) => self.geode.get(&i_file_hash, file_path).await.unwrap(),
|
||||||
|
// We could not find the file metadata
|
||||||
Err(Error::GeodeFileRouteNotFound) => {
|
Err(Error::GeodeFileRouteNotFound) => {
|
||||||
// Set resource status to `Incomplete` and send FudEvent::FileNotFound
|
// Set resource status to `Incomplete` and send FudEvent::FileNotFound
|
||||||
let mut resources_write = self.resources.write().await;
|
let mut resources_write = self.resources.write().await;
|
||||||
@@ -545,8 +456,8 @@ impl Fud {
|
|||||||
resource.status = ResourceStatus::Incomplete;
|
resource.status = ResourceStatus::Incomplete;
|
||||||
|
|
||||||
self.event_publisher
|
self.event_publisher
|
||||||
.notify(FudEvent::FileNotFound(FileNotFound {
|
.notify(FudEvent::FileNotFound(event::FileNotFound {
|
||||||
file_hash: *file_hash,
|
hash: *file_hash,
|
||||||
resource: resource.clone(),
|
resource: resource.clone(),
|
||||||
}))
|
}))
|
||||||
.await;
|
.await;
|
||||||
@@ -581,60 +492,57 @@ impl Fud {
|
|||||||
};
|
};
|
||||||
drop(resources_write);
|
drop(resources_write);
|
||||||
|
|
||||||
|
// Send a FileDownloadCompleted event
|
||||||
self.event_publisher
|
self.event_publisher
|
||||||
.notify(FudEvent::FileDownloadCompleted(FileDownloadCompleted {
|
.notify(FudEvent::FileDownloadCompleted(event::FileDownloadCompleted {
|
||||||
file_hash: *file_hash,
|
hash: *file_hash,
|
||||||
resource: resource.clone(),
|
resource: resource.clone(),
|
||||||
}))
|
}))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
// If the file is already complete, we don't need to download any chunk
|
||||||
if chunked_file.is_complete() {
|
if chunked_file.is_complete() {
|
||||||
|
// Announce the file
|
||||||
let self_announce =
|
let self_announce =
|
||||||
FudAnnounce { key: *file_hash, seeders: vec![self_node.clone().into()] };
|
FudAnnounce { key: *file_hash, seeders: vec![self_node.clone().into()] };
|
||||||
let _ = self.announce(file_hash, &self_announce, self.seeders_router.clone()).await;
|
let _ = self.announce(file_hash, &self_announce, self.seeders_router.clone()).await;
|
||||||
|
|
||||||
return match self.geode.assemble_file(file_hash, &chunked_file, file_path).await {
|
// Set resource status to `Seeding`
|
||||||
Ok(_) => {
|
let mut resources_write = self.resources.write().await;
|
||||||
// Set resource status to `Seeding`
|
let resource = match resources_write.get_mut(file_hash) {
|
||||||
let mut resources_write = self.resources.write().await;
|
Some(resource) => {
|
||||||
let resource = match resources_write.get_mut(file_hash) {
|
resource.status = ResourceStatus::Seeding;
|
||||||
Some(resource) => {
|
resource.chunks_downloaded = chunked_file.len() as u64;
|
||||||
resource.status = ResourceStatus::Seeding;
|
resource.clone()
|
||||||
resource.chunks_downloaded = chunked_file.len() as u64;
|
|
||||||
resource.clone()
|
|
||||||
}
|
|
||||||
None => return Ok(()), // Resource was removed, abort
|
|
||||||
};
|
|
||||||
drop(resources_write);
|
|
||||||
|
|
||||||
self.event_publisher
|
|
||||||
.notify(FudEvent::DownloadCompleted(DownloadCompleted {
|
|
||||||
file_hash: *file_hash,
|
|
||||||
file_path: file_path.clone(),
|
|
||||||
resource,
|
|
||||||
}))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!(target: "fud::handle_get()", "{}", e);
|
|
||||||
self.event_publisher
|
|
||||||
.notify(FudEvent::DownloadError(DownloadError {
|
|
||||||
file_hash: *file_hash,
|
|
||||||
error: e.to_string(),
|
|
||||||
}))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Err(e)
|
|
||||||
}
|
}
|
||||||
|
None => return Ok(()), // Resource was removed, abort
|
||||||
};
|
};
|
||||||
|
drop(resources_write);
|
||||||
|
|
||||||
|
// Send a DownloadCompleted event
|
||||||
|
self.event_publisher
|
||||||
|
.notify(FudEvent::DownloadCompleted(event::DownloadCompleted {
|
||||||
|
hash: *file_hash,
|
||||||
|
resource,
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find nodes close to the file hash if we didn't previously fetched them
|
||||||
if closest_nodes.is_empty() {
|
if closest_nodes.is_empty() {
|
||||||
closest_nodes = self.lookup_nodes(file_hash).await.unwrap_or_default();
|
closest_nodes = self.lookup_nodes(file_hash).await.unwrap_or_default();
|
||||||
}
|
}
|
||||||
let seeders = self.fetch_seeders(&closest_nodes, file_hash).await;
|
|
||||||
|
// Find seeders and remove ourselves from the result
|
||||||
|
let seeders = self
|
||||||
|
.fetch_seeders(&closest_nodes, file_hash)
|
||||||
|
.await
|
||||||
|
.iter()
|
||||||
|
.filter(|seeder| seeder.node.id != self_node.id)
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
// List missing chunks
|
// List missing chunks
|
||||||
let mut missing_chunks = HashSet::new();
|
let mut missing_chunks = HashSet::new();
|
||||||
@@ -645,9 +553,10 @@ impl Fud {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch missing chunks from seeders
|
// Fetch missing chunks from seeders
|
||||||
self.fetch_chunks(file_hash, &missing_chunks, &seeders).await?;
|
self.fetch_chunks(file_path, file_hash, &missing_chunks, &seeders).await?;
|
||||||
|
|
||||||
let chunked_file = match self.geode.get(file_hash).await {
|
// Get chunked file from geode
|
||||||
|
let chunked_file = match self.geode.get(file_hash, file_path).await {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(target: "fud::handle_get()", "{}", e);
|
error!(target: "fud::handle_get()", "{}", e);
|
||||||
@@ -669,12 +578,17 @@ impl Fud {
|
|||||||
};
|
};
|
||||||
drop(resources_write);
|
drop(resources_write);
|
||||||
|
|
||||||
|
// Send a MissingChunks event
|
||||||
self.event_publisher
|
self.event_publisher
|
||||||
.notify(FudEvent::MissingChunks(MissingChunks { file_hash: *file_hash, resource }))
|
.notify(FudEvent::MissingChunks(event::MissingChunks {
|
||||||
|
hash: *file_hash,
|
||||||
|
resource,
|
||||||
|
}))
|
||||||
.await;
|
.await;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Announce the file
|
||||||
let self_announce =
|
let self_announce =
|
||||||
FudAnnounce { key: *file_hash, seeders: vec![self_node.clone().into()] };
|
FudAnnounce { key: *file_hash, seeders: vec![self_node.clone().into()] };
|
||||||
let _ = self.announce(file_hash, &self_announce, self.seeders_router.clone()).await;
|
let _ = self.announce(file_hash, &self_announce, self.seeders_router.clone()).await;
|
||||||
@@ -691,26 +605,13 @@ impl Fud {
|
|||||||
};
|
};
|
||||||
drop(resources_write);
|
drop(resources_write);
|
||||||
|
|
||||||
match self.geode.assemble_file(file_hash, &chunked_file, file_path).await {
|
// Send a DownloadCompleted event
|
||||||
Ok(_) => {
|
self.event_publisher
|
||||||
self.event_publisher
|
.notify(FudEvent::DownloadCompleted(event::DownloadCompleted {
|
||||||
.notify(FudEvent::DownloadCompleted(DownloadCompleted {
|
hash: *file_hash,
|
||||||
file_hash: *file_hash,
|
resource,
|
||||||
file_path: file_path.clone(),
|
}))
|
||||||
resource,
|
.await;
|
||||||
}))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!(target: "fud::handle_get()", "{}", e);
|
|
||||||
self.event_publisher
|
|
||||||
.notify(FudEvent::DownloadError(DownloadError {
|
|
||||||
file_hash: *file_hash,
|
|
||||||
error: e.to_string(),
|
|
||||||
}))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ pub enum FetchReply {
|
|||||||
pub async fn fetch_file_task(fud: Arc<Fud>) -> Result<()> {
|
pub async fn fetch_file_task(fud: Arc<Fud>) -> Result<()> {
|
||||||
info!(target: "fud::fetch_file_task()", "Started background file fetch task");
|
info!(target: "fud::fetch_file_task()", "Started background file fetch task");
|
||||||
loop {
|
loop {
|
||||||
let (nodes, file_hash, _) = fud.file_fetch_rx.recv().await.unwrap();
|
let (nodes, file_hash, file_path, _) = fud.file_fetch_rx.recv().await.unwrap();
|
||||||
info!(target: "fud::fetch_file_task()", "Fetching file {}", hash_to_string(&file_hash));
|
info!(target: "fud::fetch_file_task()", "Fetching file {}", hash_to_string(&file_hash));
|
||||||
|
|
||||||
let result = fud.fetch_file_metadata(nodes, file_hash).await;
|
let result = fud.fetch_file_metadata(nodes, file_hash).await;
|
||||||
@@ -69,7 +69,7 @@ pub async fn fetch_file_task(fud: Arc<Fud>) -> Result<()> {
|
|||||||
info!(target: "fud::fetch_file_task()", "File fits in a single chunk");
|
info!(target: "fud::fetch_file_task()", "File fits in a single chunk");
|
||||||
let chunk_hash = blake3::hash(&chunk);
|
let chunk_hash = blake3::hash(&chunk);
|
||||||
let _ = fud.geode.insert_file(&file_hash, &[chunk_hash]).await;
|
let _ = fud.geode.insert_file(&file_hash, &[chunk_hash]).await;
|
||||||
match fud.geode.insert_chunk(&chunk).await {
|
match fud.geode.write_chunk(&file_hash, &file_path, &chunk).await {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
@@ -104,7 +104,7 @@ pub async fn announce_seed_task(fud: Arc<Fud>) -> Result<()> {
|
|||||||
let seeders = vec![fud.dht().node().await.into()];
|
let seeders = vec![fud.dht().node().await.into()];
|
||||||
|
|
||||||
info!(target: "fud::announce_task()", "Verifying seeds...");
|
info!(target: "fud::announce_task()", "Verifying seeds...");
|
||||||
let seeding_resources = match fud.get_seeding_resources().await {
|
let seeding_resources = match fud.verify_resources(None).await {
|
||||||
Ok(resources) => resources,
|
Ok(resources) => resources,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(target: "fud::announce_task()", "Error while verifying seeding resources: {}", e);
|
error!(target: "fud::announce_task()", "Error while verifying seeding resources: {}", e);
|
||||||
|
|||||||
356
src/geode/mod.rs
356
src/geode/mod.rs
@@ -23,20 +23,20 @@
|
|||||||
//! `remove` support. File removal should be handled externally, and then it
|
//! `remove` support. File removal should be handled externally, and then it
|
||||||
//! is only required to run `garbage_collect()` to clean things up.
|
//! is only required to run `garbage_collect()` to clean things up.
|
||||||
//!
|
//!
|
||||||
//! The filesystem hierarchy stores two directories: `files` and `chunks`.
|
//! The filesystem hierarchy stores a `files` directory storing metadata
|
||||||
//! `chunks` store [`MAX_CHUNK_SIZE`] files, where the filename is a BLAKE3
|
//! about a full file. The filename of a file in `files` is the BLAKE3
|
||||||
//! hash of the chunk's contents.
|
//! hash of hashed chunks in the correct order. Inside the file is the list
|
||||||
//! `files` store metadata about a full file, which can be retrieved by
|
//! of the chunks making up the full file.
|
||||||
//! concatenating the chunks in order. The filename of a file in `files`
|
//!
|
||||||
//! is the BLAKE3 hash of hashed chunks in the correct order.
|
//! To get the chunks you split the full file into `MAX_CHUNK_SIZE` sized
|
||||||
|
//! slices, where the last chunk is the only one that can be smaller than
|
||||||
|
//! that.
|
||||||
//!
|
//!
|
||||||
//! It might look like the following:
|
//! It might look like the following:
|
||||||
//! ```
|
//! ```
|
||||||
//! /files/B9fFKaEYphw2oH5PDbeL1TTAcSzL6ax84p8SjBKzuYzX
|
//! /files/B9fFKaEYphw2oH5PDbeL1TTAcSzL6ax84p8SjBKzuYzX
|
||||||
|
//! /files/8nA3ndjFFee3n5wMPLZampLpGaMJi3od4MSyaXPDoF91
|
||||||
//! /files/...
|
//! /files/...
|
||||||
//! /chunks/2bQPxSR8Frz7S7JW3DRAzEtkrHfLXB1CN65V7az77pUp
|
|
||||||
//! /chunks/CvjvN6MfWQYK54DgKNR7MPgFSZqsCgpWKF2p8ot66CCP
|
|
||||||
//! /chunks/...
|
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! In the above example, contents of `B9fFKaEYphw2oH5PDbeL1TTAcSzL6ax84p8SjBKzuYzX`
|
//! In the above example, contents of `B9fFKaEYphw2oH5PDbeL1TTAcSzL6ax84p8SjBKzuYzX`
|
||||||
@@ -46,19 +46,15 @@
|
|||||||
//! CvjvN6MfWQYK54DgKNR7MPgFSZqsCgpWKF2p8ot66CCP
|
//! CvjvN6MfWQYK54DgKNR7MPgFSZqsCgpWKF2p8ot66CCP
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! This means, in order to retrieve `B9fFKaEYphw2oH5PDbeL1TTAcSzL6ax84p8SjBKzuYzX`,
|
//! This means, the file `B9fFKaEYphw2oH5PDbeL1TTAcSzL6ax84p8SjBKzuYzX`
|
||||||
//! we need to concatenate the files under `/chunks` whose filenames are the
|
//! is the concatenation of the chunks with the above hashes.
|
||||||
//! hashes found above. The contents of the files in `/chunks` are arbitrary
|
|
||||||
//! data, and by concatenating them we can retrieve the original file.
|
|
||||||
//!
|
//!
|
||||||
//! It is important to note that multiple files can use the same chunks.
|
//! The full file is not copied, and individual chunks are not stored by
|
||||||
//! This is some kind of naive deduplication, so we actually don't consider
|
//! geode. Additionally it does not keep track of the full files path.
|
||||||
//! chunks to be specific to a single file and therefore when we do garbage
|
|
||||||
//! collection, we keep chunks and files independent of each other.
|
|
||||||
|
|
||||||
use std::{collections::HashSet, path::PathBuf};
|
use std::{collections::HashSet, path::PathBuf};
|
||||||
|
|
||||||
use futures::AsyncRead;
|
use futures::{AsyncRead, AsyncSeek};
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use smol::{
|
use smol::{
|
||||||
fs::{self, File, OpenOptions},
|
fs::{self, File, OpenOptions},
|
||||||
@@ -76,8 +72,6 @@ pub const MAX_CHUNK_SIZE: usize = 262_144;
|
|||||||
|
|
||||||
/// Path prefix where file metadata is stored
|
/// Path prefix where file metadata is stored
|
||||||
const FILES_PATH: &str = "files";
|
const FILES_PATH: &str = "files";
|
||||||
/// Path prefix where file chunks are stored
|
|
||||||
const CHUNKS_PATH: &str = "chunks";
|
|
||||||
|
|
||||||
pub fn hash_to_string(hash: &blake3::Hash) -> String {
|
pub fn hash_to_string(hash: &blake3::Hash) -> String {
|
||||||
bs58::encode(hash.as_bytes()).into_string()
|
bs58::encode(hash.as_bytes()).into_string()
|
||||||
@@ -91,7 +85,7 @@ pub fn hash_to_string(hash: &blake3::Hash) -> String {
|
|||||||
/// the filesystem where the chunk can be found. If `None`, it
|
/// the filesystem where the chunk can be found. If `None`, it
|
||||||
/// is to be assumed that the chunk is not available locally.
|
/// is to be assumed that the chunk is not available locally.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ChunkedFile(Vec<(blake3::Hash, Option<PathBuf>)>);
|
pub struct ChunkedFile(Vec<(blake3::Hash, Option<bool>)>);
|
||||||
|
|
||||||
impl ChunkedFile {
|
impl ChunkedFile {
|
||||||
fn new(hashes: &[blake3::Hash]) -> Self {
|
fn new(hashes: &[blake3::Hash]) -> Self {
|
||||||
@@ -104,7 +98,7 @@ impl ChunkedFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return an iterator over the chunks and their paths.
|
/// Return an iterator over the chunks and their paths.
|
||||||
pub fn iter(&self) -> core::slice::Iter<'_, (blake3::Hash, Option<PathBuf>)> {
|
pub fn iter(&self) -> core::slice::Iter<'_, (blake3::Hash, Option<bool>)> {
|
||||||
self.0.iter()
|
self.0.iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,8 +122,6 @@ impl ChunkedFile {
|
|||||||
pub struct Geode {
|
pub struct Geode {
|
||||||
/// Path to the filesystem directory where file metadata is stored
|
/// Path to the filesystem directory where file metadata is stored
|
||||||
files_path: PathBuf,
|
files_path: PathBuf,
|
||||||
/// Path to the filesystem directory where file chunks are stored
|
|
||||||
chunks_path: PathBuf,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// smol::fs::File::read does not guarantee that the buffer will be filled, even if the buffer is
|
/// smol::fs::File::read does not guarantee that the buffer will be filled, even if the buffer is
|
||||||
@@ -158,15 +150,12 @@ impl Geode {
|
|||||||
/// file metadata and chunks.
|
/// file metadata and chunks.
|
||||||
pub async fn new(base_path: &PathBuf) -> Result<Self> {
|
pub async fn new(base_path: &PathBuf) -> Result<Self> {
|
||||||
let mut files_path: PathBuf = base_path.into();
|
let mut files_path: PathBuf = base_path.into();
|
||||||
let mut chunks_path: PathBuf = base_path.into();
|
|
||||||
files_path.push(FILES_PATH);
|
files_path.push(FILES_PATH);
|
||||||
chunks_path.push(CHUNKS_PATH);
|
|
||||||
|
|
||||||
// Create necessary directory structure if needed
|
// Create necessary directory structure if needed
|
||||||
fs::create_dir_all(&files_path).await?;
|
fs::create_dir_all(&files_path).await?;
|
||||||
fs::create_dir_all(&chunks_path).await?;
|
|
||||||
|
|
||||||
Ok(Self { files_path, chunks_path })
|
Ok(Self { files_path })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempt to read chunk hashes from a given file path and return
|
/// Attempt to read chunk hashes from a given file path and return
|
||||||
@@ -188,75 +177,11 @@ impl Geode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Perform garbage collection over the filesystem hierarchy.
|
/// Perform garbage collection over the filesystem hierarchy.
|
||||||
/// Returns sets representing deleted files and deleted chunks, respectively.
|
/// Returns a set representing deleted files.
|
||||||
pub async fn garbage_collect(&self) -> Result<(HashSet<blake3::Hash>, HashSet<blake3::Hash>)> {
|
pub async fn garbage_collect(&self) -> Result<HashSet<blake3::Hash>> {
|
||||||
info!(target: "geode::garbage_collect()", "[Geode] Performing garbage collection");
|
info!(target: "geode::garbage_collect()", "[Geode] Performing garbage collection");
|
||||||
// We track corrupt files and chunks here.
|
// We track corrupt files here.
|
||||||
let mut deleted_files = HashSet::new();
|
let mut deleted_files = HashSet::new();
|
||||||
let mut deleted_chunks = HashSet::new();
|
|
||||||
let mut deleted_chunk_paths = HashSet::new();
|
|
||||||
|
|
||||||
// Scan through available chunks and check them for consistency.
|
|
||||||
let mut chunk_paths = fs::read_dir(&self.chunks_path).await?;
|
|
||||||
let mut buf = [0u8; MAX_CHUNK_SIZE];
|
|
||||||
while let Some(chunk) = chunk_paths.next().await {
|
|
||||||
let Ok(entry) = chunk else { continue };
|
|
||||||
let chunk_path = entry.path();
|
|
||||||
|
|
||||||
// Skip if we're not a plain file
|
|
||||||
if !chunk_path.is_file() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure that the filename is a BLAKE3 hash
|
|
||||||
let file_name = match chunk_path.file_name().and_then(|n| n.to_str()) {
|
|
||||||
Some(v) => v,
|
|
||||||
None => continue,
|
|
||||||
};
|
|
||||||
let mut hash_buf = [0u8; 32];
|
|
||||||
let chunk_hash = match bs58::decode(file_name).onto(&mut hash_buf) {
|
|
||||||
Ok(_) => blake3::Hash::from_bytes(hash_buf),
|
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
// If there is a problem with opening the file, remove it.
|
|
||||||
let Ok(mut chunk_fd) = File::open(&chunk_path).await else {
|
|
||||||
deleted_chunk_paths.insert(chunk_path);
|
|
||||||
deleted_chunks.insert(chunk_hash);
|
|
||||||
continue
|
|
||||||
};
|
|
||||||
|
|
||||||
// Perform consistency check
|
|
||||||
let Ok(bytes_read) = read_until_filled(&mut chunk_fd, &mut buf).await else {
|
|
||||||
deleted_chunk_paths.insert(chunk_path);
|
|
||||||
deleted_chunks.insert(chunk_hash);
|
|
||||||
buf = [0u8; MAX_CHUNK_SIZE];
|
|
||||||
continue
|
|
||||||
};
|
|
||||||
|
|
||||||
let chunk_slice = &buf[..bytes_read];
|
|
||||||
let hashed_chunk = blake3::hash(chunk_slice);
|
|
||||||
|
|
||||||
// If the hash doesn't match the filename, remove it.
|
|
||||||
if chunk_hash != hashed_chunk {
|
|
||||||
deleted_chunk_paths.insert(chunk_path);
|
|
||||||
deleted_chunks.insert(chunk_hash);
|
|
||||||
buf = [0u8; MAX_CHUNK_SIZE];
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seems legit.
|
|
||||||
buf = [0u8; MAX_CHUNK_SIZE];
|
|
||||||
}
|
|
||||||
|
|
||||||
for chunk_path in &deleted_chunk_paths {
|
|
||||||
if let Err(e) = fs::remove_file(chunk_path).await {
|
|
||||||
warn!(
|
|
||||||
target: "geode::garbage_collect()",
|
|
||||||
"[Geode] Garbage collect failed to remove corrupted chunk: {}", e,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform health check over file metadata. For now we just ensure they
|
// Perform health check over file metadata. For now we just ensure they
|
||||||
// have the correct format.
|
// have the correct format.
|
||||||
@@ -298,7 +223,7 @@ impl Geode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
info!(target: "geode::garbage_collect()", "[Geode] Garbage collection finished");
|
info!(target: "geode::garbage_collect()", "[Geode] Garbage collection finished");
|
||||||
Ok((deleted_files, deleted_chunks))
|
Ok(deleted_files)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a file into Geode. The function expects any kind of byte stream, which
|
/// Insert a file into Geode. The function expects any kind of byte stream, which
|
||||||
@@ -312,9 +237,9 @@ impl Geode {
|
|||||||
info!(target: "geode::insert()", "[Geode] Inserting file...");
|
info!(target: "geode::insert()", "[Geode] Inserting file...");
|
||||||
let mut file_hasher = blake3::Hasher::new();
|
let mut file_hasher = blake3::Hasher::new();
|
||||||
let mut chunk_hashes = vec![];
|
let mut chunk_hashes = vec![];
|
||||||
let mut buf = [0u8; MAX_CHUNK_SIZE];
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
let mut buf = [0u8; MAX_CHUNK_SIZE];
|
||||||
let bytes_read = read_until_filled(&mut stream, &mut buf).await?;
|
let bytes_read = read_until_filled(&mut stream, &mut buf).await?;
|
||||||
if bytes_read == 0 {
|
if bytes_read == 0 {
|
||||||
break
|
break
|
||||||
@@ -324,47 +249,6 @@ impl Geode {
|
|||||||
let chunk_hash = blake3::hash(chunk_slice);
|
let chunk_hash = blake3::hash(chunk_slice);
|
||||||
file_hasher.update(chunk_hash.as_bytes());
|
file_hasher.update(chunk_hash.as_bytes());
|
||||||
chunk_hashes.push(chunk_hash);
|
chunk_hashes.push(chunk_hash);
|
||||||
|
|
||||||
// Write the chunk to a file, if necessary. We first perform
|
|
||||||
// a consistency check and if things are fine, we don't have
|
|
||||||
// to perform a write, which is usually more expensive than
|
|
||||||
// reading from disk.
|
|
||||||
let mut chunk_path = self.chunks_path.clone();
|
|
||||||
chunk_path.push(hash_to_string(&chunk_hash).as_str());
|
|
||||||
let chunk_fd =
|
|
||||||
OpenOptions::new().read(true).write(true).create(true).open(&chunk_path).await?;
|
|
||||||
|
|
||||||
let mut fs_buf = [0u8; MAX_CHUNK_SIZE];
|
|
||||||
let fs_bytes_read = read_until_filled(chunk_fd, &mut fs_buf).await?;
|
|
||||||
let fs_chunk_slice = &fs_buf[..fs_bytes_read];
|
|
||||||
let fs_chunk_hash = blake3::hash(fs_chunk_slice);
|
|
||||||
|
|
||||||
if fs_chunk_hash != chunk_hash {
|
|
||||||
debug!(
|
|
||||||
target: "geode::insert()",
|
|
||||||
"Existing chunk inconsistent or unavailable. Writing chunk to {:?}",
|
|
||||||
chunk_path,
|
|
||||||
);
|
|
||||||
// Here the chunk is broken, so we'll truncate and write the new one.
|
|
||||||
let mut chunk_fd = OpenOptions::new()
|
|
||||||
.read(true)
|
|
||||||
.write(true)
|
|
||||||
.create(true)
|
|
||||||
.open(&chunk_path)
|
|
||||||
.await?;
|
|
||||||
chunk_fd.set_len(0).await?;
|
|
||||||
chunk_fd.seek(SeekFrom::Start(0)).await?;
|
|
||||||
chunk_fd.write_all(chunk_slice).await?;
|
|
||||||
chunk_fd.flush().await?;
|
|
||||||
} else {
|
|
||||||
debug!(
|
|
||||||
target: "geode::insert()",
|
|
||||||
"Existing chunk consistent. Skipping write to {:?}",
|
|
||||||
chunk_path,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
buf = [0u8; MAX_CHUNK_SIZE];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This hash is the file's chunks hashes hashed in order.
|
// This hash is the file's chunks hashes hashed in order.
|
||||||
@@ -410,10 +294,16 @@ impl Geode {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create and insert a single chunk into Geode given a stream.
|
/// Write a single chunk into `file_path` given a stream.
|
||||||
|
/// The file must be inserted into Geode before calling this method.
|
||||||
/// Always overwrites any existing chunk. Returns the chunk hash once inserted.
|
/// Always overwrites any existing chunk. Returns the chunk hash once inserted.
|
||||||
pub async fn insert_chunk(&self, stream: impl AsRef<[u8]>) -> Result<blake3::Hash> {
|
pub async fn write_chunk(
|
||||||
info!(target: "geode::insert_chunk()", "[Geode] Inserting single chunk");
|
&self,
|
||||||
|
file_hash: &blake3::Hash,
|
||||||
|
file_path: &PathBuf,
|
||||||
|
stream: impl AsRef<[u8]>,
|
||||||
|
) -> Result<blake3::Hash> {
|
||||||
|
info!(target: "geode::write_chunk()", "[Geode] Writing single chunk");
|
||||||
|
|
||||||
let mut cursor = Cursor::new(&stream);
|
let mut cursor = Cursor::new(&stream);
|
||||||
let mut chunk = [0u8; MAX_CHUNK_SIZE];
|
let mut chunk = [0u8; MAX_CHUNK_SIZE];
|
||||||
@@ -422,27 +312,43 @@ impl Geode {
|
|||||||
let chunk_slice = &chunk[..bytes_read];
|
let chunk_slice = &chunk[..bytes_read];
|
||||||
let chunk_hash = blake3::hash(chunk_slice);
|
let chunk_hash = blake3::hash(chunk_slice);
|
||||||
|
|
||||||
let mut chunk_path = self.chunks_path.clone();
|
let chunked_file = self.get(file_hash, file_path).await?;
|
||||||
chunk_path.push(hash_to_string(&chunk_hash).as_str());
|
|
||||||
let mut chunk_fd = File::create(&chunk_path).await?;
|
// Get the chunk index in the file from the chunk hash
|
||||||
chunk_fd.write_all(chunk_slice).await?;
|
let chunk_index = match chunked_file.iter().position(|c| c.0 == chunk_hash) {
|
||||||
chunk_fd.flush().await?;
|
Some(index) => index,
|
||||||
|
None => {
|
||||||
|
return Err(Error::GeodeNeedsGc);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let position = (chunk_index as u64) * (MAX_CHUNK_SIZE as u64);
|
||||||
|
|
||||||
|
// Create the file if it does not exist
|
||||||
|
if !file_path.exists() {
|
||||||
|
File::create(&file_path).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut file_fd = OpenOptions::new().write(true).open(&file_path).await?;
|
||||||
|
file_fd.seek(SeekFrom::Start(position)).await?;
|
||||||
|
file_fd.write_all(chunk_slice).await?;
|
||||||
|
file_fd.flush().await?;
|
||||||
|
|
||||||
Ok(chunk_hash)
|
Ok(chunk_hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch file metadata from Geode. Returns [`ChunkedFile`] which gives a list
|
/// Fetch file metadata from Geode. Returns [`ChunkedFile`] which gives a list
|
||||||
/// of chunks and optionally file paths to the said chunks. Returns an error if
|
/// of chunks and booleans to know if the chunks we have are valid. Returns an error if
|
||||||
/// the read failed in any way (could also be the file does not exist).
|
/// the read failed in any way (could also be the file does not exist).
|
||||||
pub async fn get(&self, file_hash: &blake3::Hash) -> Result<ChunkedFile> {
|
pub async fn get(&self, file_hash: &blake3::Hash, file_path: &PathBuf) -> Result<ChunkedFile> {
|
||||||
let file_hash_str = hash_to_string(file_hash);
|
let file_hash_str = hash_to_string(file_hash);
|
||||||
info!(target: "geode::get()", "[Geode] Getting file chunks for {}...", file_hash_str);
|
info!(target: "geode::get()", "[Geode] Getting file chunks for {}...", file_hash_str);
|
||||||
let mut file_path = self.files_path.clone();
|
let mut file_metadata_path = self.files_path.clone();
|
||||||
file_path.push(file_hash_str);
|
file_metadata_path.push(file_hash_str);
|
||||||
|
|
||||||
// Try to read the file metadata. If it's corrupt, return an error signalling
|
// Try to read the file metadata. If it's corrupt, return an error signalling
|
||||||
// that garbage collection needs to run.
|
// that garbage collection needs to run.
|
||||||
let chunk_hashes = match Self::read_metadata(&file_path).await {
|
let chunk_hashes = match Self::read_metadata(&file_metadata_path).await {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return match e {
|
return match e {
|
||||||
@@ -461,63 +367,95 @@ impl Geode {
|
|||||||
|
|
||||||
let mut chunked_file = ChunkedFile::new(&chunk_hashes);
|
let mut chunked_file = ChunkedFile::new(&chunk_hashes);
|
||||||
|
|
||||||
// Iterate over chunks and find which chunks we have available locally.
|
// Open the file, if we can't we return the chunked file with no locally available chunk.
|
||||||
let mut buf = vec![];
|
let mut file = match File::open(&file_path).await {
|
||||||
for (chunk_hash, chunk_path) in chunked_file.0.iter_mut() {
|
Ok(v) => v,
|
||||||
let mut c_path = self.chunks_path.clone();
|
Err(_) => {
|
||||||
c_path.push(hash_to_string(chunk_hash).as_str());
|
return Ok(chunked_file);
|
||||||
|
|
||||||
if !c_path.exists() || !c_path.is_file() {
|
|
||||||
// TODO: We should be aggressive here and remove the non-file.
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Iterate over chunks and find which chunks we have available locally.
|
||||||
|
for (chunk_index, (chunk_hash, chunk_valid)) in chunked_file.0.iter_mut().enumerate() {
|
||||||
|
let chunk = self.read_chunk(&mut file, &chunk_index).await?;
|
||||||
|
|
||||||
// Perform chunk consistency check
|
// Perform chunk consistency check
|
||||||
let mut chunk_fd = File::open(&c_path).await?;
|
if !self.verify_chunk(chunk_hash, &chunk) {
|
||||||
let bytes_read = chunk_fd.read_to_end(&mut buf).await?;
|
|
||||||
let chunk_slice = &buf[..bytes_read];
|
|
||||||
let hashed_chunk = blake3::hash(chunk_slice);
|
|
||||||
if &hashed_chunk != chunk_hash {
|
|
||||||
// The chunk is corrupted/inconsistent. Garbage collection should run.
|
|
||||||
buf = vec![];
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
*chunk_path = Some(c_path);
|
*chunk_valid = Some(true);
|
||||||
buf = vec![];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(chunked_file)
|
Ok(chunked_file)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch a single chunk from Geode. Returns a `PathBuf` pointing to the chunk
|
/// Fetch a single chunk from Geode. Returns a Vec containing the chunk content
|
||||||
/// if it is found.
|
/// if it is found.
|
||||||
pub async fn get_chunk(&self, chunk_hash: &blake3::Hash) -> Result<PathBuf> {
|
pub async fn get_chunk(
|
||||||
let chunk_hash_str = hash_to_string(chunk_hash);
|
&self,
|
||||||
info!(target: "geode::get_chunk()", "[Geode] Getting chunk {}", chunk_hash_str);
|
chunk_hash: &blake3::Hash,
|
||||||
let mut chunk_path = self.chunks_path.clone();
|
file_hash: &blake3::Hash,
|
||||||
chunk_path.push(chunk_hash_str);
|
file_path: &PathBuf,
|
||||||
|
) -> Result<Vec<u8>> {
|
||||||
|
info!(target: "geode::get_chunk()", "[Geode] Getting chunk {}", hash_to_string(chunk_hash));
|
||||||
|
|
||||||
if !chunk_path.exists() || !chunk_path.is_file() {
|
if !file_path.exists() || !file_path.is_file() {
|
||||||
// TODO: We should be aggressive here and remove the non-file.
|
|
||||||
return Err(Error::GeodeChunkNotFound)
|
return Err(Error::GeodeChunkNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut file_metadata_path = self.files_path.clone();
|
||||||
|
file_metadata_path.push(hash_to_string(file_hash));
|
||||||
|
|
||||||
|
// Try to read the file metadata. If it's corrupt, return an error signalling
|
||||||
|
// that garbage collection needs to run.
|
||||||
|
let chunk_hashes = match Self::read_metadata(&file_metadata_path).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
return match e {
|
||||||
|
// If the file is not found, return according error.
|
||||||
|
Error::Io(std::io::ErrorKind::NotFound) => Err(Error::GeodeFileNotFound),
|
||||||
|
// Anything else should tell the client to do garbage collection
|
||||||
|
_ => Err(Error::GeodeNeedsGc),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the chunk index in the file from the chunk hash
|
||||||
|
let chunk_index = match chunk_hashes.iter().position(|&h| h == *chunk_hash) {
|
||||||
|
Some(index) => index,
|
||||||
|
None => return Err(Error::GeodeChunkNotFound),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read the file to get the chunk content
|
||||||
|
let mut file = File::open(&file_path).await?;
|
||||||
|
let chunk = self.read_chunk(&mut file, &chunk_index).await?;
|
||||||
|
|
||||||
// Perform chunk consistency check
|
// Perform chunk consistency check
|
||||||
let mut buf = vec![];
|
if !self.verify_chunk(chunk_hash, &chunk) {
|
||||||
let mut chunk_fd = File::open(&chunk_path).await?;
|
|
||||||
let bytes_read = chunk_fd.read_to_end(&mut buf).await?;
|
|
||||||
if !self.verify_chunk(chunk_hash, &buf[..bytes_read]) {
|
|
||||||
// The chunk is corrupted
|
|
||||||
return Err(Error::GeodeNeedsGc)
|
return Err(Error::GeodeNeedsGc)
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(chunk_path)
|
Ok(chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the file at `file_path` to get its chunk with index `chunk_index`.
|
||||||
|
/// Returns the chunk content in a Vec.
|
||||||
|
pub async fn read_chunk(
|
||||||
|
&self,
|
||||||
|
mut stream: impl AsyncRead + Unpin + AsyncSeek,
|
||||||
|
chunk_index: &usize,
|
||||||
|
) -> Result<Vec<u8>> {
|
||||||
|
let position = (*chunk_index as u64) * (MAX_CHUNK_SIZE as u64);
|
||||||
|
let mut buf = [0u8; MAX_CHUNK_SIZE];
|
||||||
|
stream.seek(SeekFrom::Start(position)).await?;
|
||||||
|
let bytes_read = read_until_filled(stream, &mut buf).await?;
|
||||||
|
Ok(buf[..bytes_read].to_vec())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verifies that the file hash matches the chunk hashes.
|
/// Verifies that the file hash matches the chunk hashes.
|
||||||
pub fn verify_file(&self, file_hash: &blake3::Hash, chunk_hashes: &[blake3::Hash]) -> bool {
|
pub fn verify_file(&self, file_hash: &blake3::Hash, chunk_hashes: &[blake3::Hash]) -> bool {
|
||||||
info!(target: "geode::verify_file()", "[Geode] Verifying file metadata");
|
info!(target: "geode::verify_file()", "[Geode] Verifying file metadata for {}", hash_to_string(file_hash));
|
||||||
|
|
||||||
let mut file_hasher = blake3::Hasher::new();
|
let mut file_hasher = blake3::Hasher::new();
|
||||||
for chunk_hash in chunk_hashes {
|
for chunk_hash in chunk_hashes {
|
||||||
@@ -531,54 +469,4 @@ impl Geode {
|
|||||||
pub fn verify_chunk(&self, chunk_hash: &blake3::Hash, chunk_slice: &[u8]) -> bool {
|
pub fn verify_chunk(&self, chunk_hash: &blake3::Hash, chunk_slice: &[u8]) -> bool {
|
||||||
blake3::hash(chunk_slice) == *chunk_hash
|
blake3::hash(chunk_slice) == *chunk_hash
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Assemble chunks to create a file.
|
|
||||||
/// This method does NOT perform a consistency check.
|
|
||||||
pub async fn assemble_file(
|
|
||||||
&self,
|
|
||||||
file_hash: &blake3::Hash,
|
|
||||||
chunked_file: &ChunkedFile,
|
|
||||||
file_path: &PathBuf,
|
|
||||||
) -> Result<()> {
|
|
||||||
let file_hash_str = hash_to_string(file_hash);
|
|
||||||
info!(target: "geode::assemble_file()", "[Geode] Assembling file {}", file_hash_str);
|
|
||||||
|
|
||||||
if file_path.exists() && file_path.is_dir() {
|
|
||||||
return Err(Error::Custom("File path is an existing directory".to_string())) // TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut file_fd = File::create(&file_path).await?;
|
|
||||||
for (_, chunk_path) in chunked_file.iter() {
|
|
||||||
let mut buf = vec![];
|
|
||||||
let mut chunk_fd = File::open(chunk_path.clone().unwrap()).await?;
|
|
||||||
let bytes_read = chunk_fd.read_to_end(&mut buf).await?;
|
|
||||||
let chunk_slice = &buf[..bytes_read];
|
|
||||||
file_fd.write(chunk_slice).await?;
|
|
||||||
file_fd.flush().await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List file hashes.
|
|
||||||
pub async fn list_files(&self) -> Result<Vec<blake3::Hash>> {
|
|
||||||
info!(target: "geode::list_files()", "[Geode] Listing files");
|
|
||||||
|
|
||||||
let mut dir = fs::read_dir(&self.files_path).await?;
|
|
||||||
|
|
||||||
let mut file_hashes = vec![];
|
|
||||||
|
|
||||||
while let Some(file) = dir.try_next().await? {
|
|
||||||
let os_file_name = file.file_name();
|
|
||||||
let file_name = os_file_name.to_string_lossy().to_string();
|
|
||||||
let mut hash_buf = [0u8; 32];
|
|
||||||
let file_hash = match bs58::decode(file_name).onto(&mut hash_buf) {
|
|
||||||
Ok(_) => blake3::Hash::from_bytes(hash_buf),
|
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
file_hashes.push(file_hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(file_hashes)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user