diff options
| author | murilo ijanc | 2026-03-27 21:54:25 -0300 |
|---|---|---|
| committer | murilo ijanc | 2026-03-27 21:54:25 -0300 |
| commit | b4228aa74f6ef4720167236cb072b84d94aa6d2a (patch) | |
| tree | 5a43a68455a06009d0c288e786a4bc000a406a8c /src/store.rs | |
| parent | 75fddf425102369828f7e8366ebdad4ea086fd07 (diff) | |
| download | tesseras-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.rs | 121 |
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 |