use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use crate::base58;
use crate::paste::Paste;
#[derive(Clone)]
pub struct PasteStore {
root: PathBuf,
}
impl PasteStore {
pub fn open(root: &Path) -> std::io::Result<Self> {
fs::create_dir_all(root.join("pastes"))?;
fs::create_dir_all(root.join("pins"))?;
fs::create_dir_all(root.join("blocked"))?;
Ok(PasteStore {
root: root.to_path_buf(),
})
}
fn paste_path(&self, key: &[u8]) -> PathBuf {
self.root.join("pastes").join(base58::encode(key))
}
fn pin_path(&self, key: &[u8]) -> PathBuf {
self.root.join("pins").join(base58::encode(key))
}
fn block_path(&self, key: &[u8]) -> PathBuf {
self.root.join("blocked").join(base58::encode(key))
}
pub fn put_paste(&self, key: &[u8], value: &[u8]) -> std::io::Result<()> {
let path = self.paste_path(key);
atomic_write(&path, &[key, value])
}
pub fn get_paste(&self, key: &[u8]) -> Option<Vec<u8>> {
if self.is_blocked(key) {
return None;
}
let path = self.paste_path(key);
let data = fs::read(&path).ok()?;
if data.len() < 32 {
return None;
}
let value = data[32..].to_vec();
if let Some(paste) = Paste::from_bytes(&value)
&& paste.is_expired()
&& !self.is_pinned(key)
{
return None;
}
Some(value)
}
pub fn remove_paste(&self, key: &[u8]) {
let _ = fs::remove_file(self.paste_path(key));
}
pub fn original_keys(&self) -> Vec<Vec<u8>> {
let dir = self.root.join("pastes");
let entries = match fs::read_dir(&dir) {
Ok(e) => e,
Err(_) => return Vec::new(),
};
let mut keys = Vec::new();
for entry in entries.flatten() {
let data = match fs::read(entry.path()) {
Ok(d) => d,
Err(_) => continue,
};
if data.len() < 32 {
continue;
}
let key = &data[..32];
let value = &data[32..];
if self.is_blocked(key) {
continue;
}
if let Some(paste) = Paste::from_bytes(value)
&& paste.is_expired()
&& !self.is_pinned(key)
{
continue;
}
keys.push(key.to_vec());
}
keys
}
pub fn pin(&self, key: &[u8]) -> std::io::Result<()> {
fs::File::create(self.pin_path(key))?;
Ok(())
}
pub fn unpin(&self, key: &[u8]) -> std::io::Result<()> {
let _ = fs::remove_file(self.pin_path(key));
Ok(())
}
pub fn is_pinned(&self, key: &[u8]) -> bool {
self.pin_path(key).exists()
}
pub fn block(&self, key: &[u8]) {
let _ = fs::File::create(self.block_path(key));
}
pub fn is_blocked(&self, key: &[u8]) -> bool {
self.block_path(key).exists()
}
pub fn gc(&self) -> std::io::Result<usize> {
let dir = self.root.join("pastes");
let entries = fs::read_dir(&dir)?;
let mut removed = 0;
for entry in entries.flatten() {
let data = match fs::read(entry.path()) {
Ok(d) => d,
Err(_) => continue,
};
if data.len() < 32 {
continue;
}
let key = &data[..32];
let value = &data[32..];
if self.is_pinned(key) {
continue;
}
if let Some(paste) = Paste::from_bytes(value)
&& paste.is_expired()
{
let _ = fs::remove_file(entry.path());
removed += 1;
}
}
Ok(removed)
}
pub fn paste_count(&self) -> usize {
let dir = self.root.join("pastes");
fs::read_dir(&dir).map(|e| e.count()).unwrap_or(0)
}
}
fn atomic_write(path: &Path, chunks: &[&[u8]]) -> std::io::Result<()> {
let parent = path.parent().unwrap_or(Path::new("."));
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("tmp");
let tmp = parent.join(format!(".tmp.{}.{}", std::process::id(), name));
let mut f = fs::File::create(&tmp)?;
for chunk in chunks {
f.write_all(chunk)?;
}
f.sync_all()?;
fs::rename(&tmp, path)
}
impl tesseras_dht::persist::RoutingPersistence for PasteStore {
fn save_contacts(
&self,
contacts: &[tesseras_dht::persist::ContactRecord],
) -> Result<(), tesseras_dht::Error> {
let path = self.root.join("contacts.bin");
let mut buf = Vec::new();
for c in contacts {
let id = c.id.as_bytes();
let addr = c.addr.to_string();
let addr_bytes = addr.as_bytes();
let len = addr_bytes.len() as u16;
buf.extend_from_slice(&len.to_be_bytes());
buf.extend_from_slice(id);
buf.extend_from_slice(addr_bytes);
}
atomic_write(&path, &[&buf]).map_err(tesseras_dht::Error::Io)?;
log::info!("store: persisted {} routing contacts", contacts.len());
Ok(())
}
fn load_contacts(
&self,
) -> Result<Vec<tesseras_dht::persist::ContactRecord>, tesseras_dht::Error>
{
let path = self.root.join("contacts.bin");
let data = match fs::read(&path) {
Ok(d) => d,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(Vec::new());
}
Err(e) => return Err(tesseras_dht::Error::Io(e)),
};
let mut out = Vec::new();
let mut pos = 0;
while pos + 2 + 32 <= data.len() {
let addr_len =
u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
pos += 2;
if pos + 32 + addr_len > data.len() {
break;
}
let mut id_bytes = [0u8; 32];
id_bytes.copy_from_slice(&data[pos..pos + 32]);
pos += 32;
let addr_str =
std::str::from_utf8(&data[pos..pos + addr_len]).unwrap_or("");
pos += addr_len;
if let Ok(addr) = addr_str.parse() {
out.push(tesseras_dht::persist::ContactRecord {
id: tesseras_dht::NodeId::from_bytes(id_bytes),
addr,
});
}
}
if !out.is_empty() {
log::info!("store: loaded {} routing contacts", out.len());
}
Ok(out)
}
}
impl tesseras_dht::persist::DataPersistence for PasteStore {
fn save(
&self,
_records: &[tesseras_dht::persist::StoredRecord],
) -> Result<(), tesseras_dht::Error> {
Ok(()) }
fn load(
&self,
) -> Result<Vec<tesseras_dht::persist::StoredRecord>, tesseras_dht::Error>
{
Ok(Vec::new()) }
}