aboutsummaryrefslogtreecommitdiffstats
path: root/src/store.rs
diff options
context:
space:
mode:
authormurilo ijanc2026-03-27 21:54:25 -0300
committermurilo ijanc2026-03-27 21:54:25 -0300
commitb4228aa74f6ef4720167236cb072b84d94aa6d2a (patch)
tree5a43a68455a06009d0c288e786a4bc000a406a8c /src/store.rs
parent75fddf425102369828f7e8366ebdad4ea086fd07 (diff)
downloadtesseras-paste-b4228aa74f6ef4720167236cb072b84d94aa6d2a.tar.gz
Add chunked paste support for content up to 1.44 MB
Large pastes are split into 8 KiB chunks on the client side, each stored separately in a dedicated chunks/ directory. A version-2 manifest paste lists the chunk hashes and is announced to the DHT; chunks replicate via periodic republish with per-put throttling to avoid rate-limit bans. - New PUTC/PUTM protocol commands for chunks and manifests - Client-side chunking avoids O(n^2) base58 on large content - HTTP handler reassembles chunks directly from store - DHT sync routes incoming chunks to chunks/ directory - Republish interval reduced to 5 min with 200ms throttle - tp.1 updated with new 1.44 MB limit
Diffstat (limited to 'src/store.rs')
-rw-r--r--src/store.rs121
1 files changed, 98 insertions, 23 deletions
diff --git a/src/store.rs b/src/store.rs
index 2e4f53a..75e932f 100644
--- a/src/store.rs
+++ b/src/store.rs
@@ -2,6 +2,7 @@
//!
//! Simple directory layout:
//! <root>/pastes/<hash>.bin
+//! <root>/chunks/<hash>.bin
//! <root>/pins/<hash>
//! <root>/blocked/<hash>
//! <root>/contacts.bin
@@ -21,9 +22,10 @@ pub struct PasteStore {
impl PasteStore {
/// Open or create a store rooted at the given directory.
- /// Creates `pastes/`, `pins/`, and `blocked/` subdirectories.
+ /// Creates `pastes/`, `chunks/`, `pins/`, and `blocked/` subdirectories.
pub fn open(root: &Path) -> std::io::Result<Self> {
fs::create_dir_all(root.join("pastes"))?;
+ fs::create_dir_all(root.join("chunks"))?;
fs::create_dir_all(root.join("pins"))?;
fs::create_dir_all(root.join("blocked"))?;
Ok(PasteStore {
@@ -35,6 +37,10 @@ impl PasteStore {
self.root.join("pastes").join(base58::encode(key))
}
+ fn chunk_path(&self, key: &[u8]) -> PathBuf {
+ self.root.join("chunks").join(base58::encode(key))
+ }
+
fn pin_path(&self, key: &[u8]) -> PathBuf {
self.root.join("pins").join(base58::encode(key))
}
@@ -82,6 +88,65 @@ impl PasteStore {
let _ = fs::remove_file(self.paste_path(key));
}
+ // ── Chunk CRUD ─────────────────────────────────
+
+ /// Write a chunk to the chunks directory.
+ pub fn put_chunk(&self, key: &[u8], value: &[u8]) -> std::io::Result<()> {
+ let path = self.chunk_path(key);
+ atomic_write(&path, &[key, value])
+ }
+
+ /// Read a chunk from the chunks directory.
+ pub fn get_chunk(&self, key: &[u8]) -> Option<Vec<u8>> {
+ if self.is_blocked(key) {
+ return None;
+ }
+ let path = self.chunk_path(key);
+ let data = fs::read(&path).ok()?;
+ if data.len() < 32 {
+ return None;
+ }
+ Some(data[32..].to_vec())
+ }
+
+ /// Delete a chunk file from disk (no-op if absent).
+ pub fn remove_chunk(&self, key: &[u8]) {
+ let _ = fs::remove_file(self.chunk_path(key));
+ }
+
+ /// List all non-blocked chunk keys.
+ pub fn chunk_keys(&self) -> Vec<Vec<u8>> {
+ let dir = self.root.join("chunks");
+ 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];
+ if self.is_blocked(key) {
+ continue;
+ }
+ // Check expiry on the chunk paste
+ let value = &data[32..];
+ if let Some(paste) = Paste::from_bytes(value)
+ && paste.is_expired()
+ {
+ continue;
+ }
+ keys.push(key.to_vec());
+ }
+ keys
+ }
+
/// List all non-expired, non-blocked paste keys.
pub fn original_keys(&self) -> Vec<Vec<u8>> {
let dir = self.root.join("pastes");
@@ -145,41 +210,51 @@ impl PasteStore {
// ── GC ──────────────────────────────────────────
- /// Remove expired pastes from disk. Pinned pastes are kept.
+ /// Remove expired pastes and chunks from disk. Pinned pastes are kept.
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,
+ for subdir in &["pastes", "chunks"] {
+ let dir = self.root.join(subdir);
+ let entries = match fs::read_dir(&dir) {
+ Ok(e) => e,
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;
+ 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)
}
- /// Count stored pastes.
+ /// Count stored pastes (excludes chunks).
pub fn paste_count(&self) -> usize {
let dir = self.root.join("pastes");
fs::read_dir(&dir).map(|e| e.count()).unwrap_or(0)
}
+
+ /// Count stored chunks.
+ pub fn chunk_count(&self) -> usize {
+ let dir = self.root.join("chunks");
+ fs::read_dir(&dir).map(|e| e.count()).unwrap_or(0)
+ }
}
/// Write data to `path` atomically: write to a temporary file in