1use std::fs::{File, OpenOptions};
14use std::io::{self, BufWriter, Read, Seek, SeekFrom, Write};
15use std::path::Path;
16
17use kevy_resp::{Reply, encode_command_borrowed};
18use kevy_resp_client::RespClient;
19
20const PIPELINE: usize = 512;
21
22pub struct Export {
29 pub keys: u64,
31 pub skipped: std::collections::BTreeMap<Vec<u8>, u64>,
33}
34
35pub fn run_export(
39 client: &mut RespClient,
40 prefix: Option<&[u8]>,
41 out_path: &Path,
42) -> io::Result<Export> {
43 let mut out = BufWriter::new(File::create(out_path)?);
44 let mut cursor: Vec<u8> = b"0".to_vec();
45 let mut pattern = prefix.unwrap_or_default().to_vec();
46 pattern.push(b'*');
47 let mut n = 0u64;
48 let mut skipped: std::collections::BTreeMap<Vec<u8>, u64> = Default::default();
51 loop {
52 let reply = client.request_borrowed(&[b"SCAN", &cursor, b"MATCH", &pattern, b"COUNT", b"512"])?;
53 let Reply::Array(items) = reply else {
54 return Err(io::Error::new(io::ErrorKind::InvalidData, "SCAN reply shape"));
55 };
56 let (Some(Reply::Bulk(next)), Some(Reply::Array(keys))) = (items.first(), items.get(1))
57 else {
58 return Err(io::Error::new(io::ErrorKind::InvalidData, "SCAN reply shape"));
59 };
60 let next = next.clone();
61 for k in keys {
62 let Reply::Bulk(key) = k else { continue };
63 let key = key.clone();
64 match export_key(client, &key, &mut out)? {
65 Some(None) => n += 1,
66 Some(Some(ty)) => *skipped.entry(ty).or_insert(0u64) += 1,
67 None => {}
68 }
69 }
70 cursor = next;
71 if cursor == b"0" {
72 break;
73 }
74 }
75 out.flush()?;
76 Ok(Export { keys: n, skipped })
77}
78
79fn export_key(
84 client: &mut RespClient,
85 key: &[u8],
86 out: &mut impl Write,
87) -> io::Result<Option<Option<Vec<u8>>>> {
88 match rebuild_frames(client, key, key)? {
89 Rebuilt::Frames(frame) => {
90 out.write_all(&frame)?;
91 Ok(Some(None))
92 }
93 Rebuilt::Vanished => Ok(None),
94 Rebuilt::UnsupportedType(ty) => Ok(Some(Some(ty))),
95 }
96}
97
98pub(crate) enum Rebuilt {
102 Frames(Vec<u8>),
104 Vanished,
106 UnsupportedType(Vec<u8>),
109}
110
111pub(crate) fn rebuild_frames(
115 client: &mut RespClient,
116 key: &[u8],
117 dst: &[u8],
118) -> io::Result<Rebuilt> {
119 let ty = match client.request_borrowed(&[b"TYPE", key])? {
120 Reply::Simple(t) => t,
121 _ => return Ok(Rebuilt::Vanished),
122 };
123 if ty == b"none" {
124 return Ok(Rebuilt::Vanished);
125 }
126 let mut frame = Vec::new();
127 encode_command_borrowed(&mut frame, &[b"DEL", dst]);
130 match encode_body(client, key, dst, &ty, &mut frame)? {
131 Some(()) => {}
132 None => return Ok(Rebuilt::Vanished),
133 }
134 if frame.len() == encoded_del_len(dst) {
135 return Ok(Rebuilt::UnsupportedType(ty));
136 }
137 append_ttl_frame(client, key, dst, &mut frame)?;
138 Ok(Rebuilt::Frames(frame))
139}
140
141fn encoded_del_len(dst: &[u8]) -> usize {
144 let mut probe = Vec::new();
145 encode_command_borrowed(&mut probe, &[b"DEL", dst]);
146 probe.len()
147}
148
149fn encode_body(
153 client: &mut RespClient,
154 key: &[u8],
155 dst: &[u8],
156 ty: &[u8],
157 frame: &mut Vec<u8>,
158) -> io::Result<Option<()>> {
159 match ty {
160 b"string" => {
161 let Reply::Bulk(v) = client.request_borrowed(&[b"GET", key])? else {
162 return Ok(None);
163 };
164 encode_command_borrowed(frame, &[b"SET", dst, &v]);
165 }
166 b"hash" => {
167 let Some(items) = fetch_bulks(client, &[b"HGETALL", key])? else {
168 return Ok(None);
169 };
170 encode_multi(frame, b"HSET", dst, &items);
171 }
172 b"list" => {
173 let Some(vals) = fetch_bulks(client, &[b"LRANGE", key, b"0", b"-1"])? else {
174 return Ok(None);
175 };
176 encode_multi(frame, b"RPUSH", dst, &vals);
177 }
178 b"set" => {
179 let Some(ms) = fetch_bulks(client, &[b"SMEMBERS", key])? else {
180 return Ok(None);
181 };
182 encode_multi(frame, b"SADD", dst, &ms);
183 }
184 b"zset" => {
185 let zrange: &[&[u8]] = &[b"ZRANGE", key, b"0", b"-1", b"WITHSCORES"];
186 let Some(flat) = fetch_bulks(client, zrange)? else {
187 return Ok(None);
188 };
189 encode_zadd(frame, dst, &flat);
190 }
191 _ => return Ok(Some(())),
195 }
196 Ok(Some(()))
197}
198
199fn fetch_bulks(client: &mut RespClient, cmd: &[&[u8]]) -> io::Result<Option<Vec<Vec<u8>>>> {
203 let Reply::Array(items) = client.request_borrowed(cmd)? else {
204 return Ok(None);
205 };
206 if items.is_empty() {
207 return Ok(None);
208 }
209 Ok(Some(
210 items
211 .into_iter()
212 .filter_map(|r| if let Reply::Bulk(b) = r { Some(b) } else { None })
213 .collect(),
214 ))
215}
216
217fn encode_multi(frame: &mut Vec<u8>, verb: &[u8], dst: &[u8], vals: &[Vec<u8>]) {
219 let mut argv: Vec<&[u8]> = vec![verb, dst];
220 argv.extend(vals.iter().map(Vec::as_slice));
221 encode_command_borrowed(frame, &argv);
222}
223
224fn encode_zadd(frame: &mut Vec<u8>, dst: &[u8], flat: &[Vec<u8>]) {
227 let mut argv: Vec<&[u8]> = vec![b"ZADD", dst];
228 for pair in flat.chunks(2) {
229 if pair.len() == 2 {
230 argv.push(&pair[1]);
231 argv.push(&pair[0]);
232 }
233 }
234 encode_command_borrowed(frame, &argv);
235}
236
237fn append_ttl_frame(
239 client: &mut RespClient,
240 key: &[u8],
241 dst: &[u8],
242 frame: &mut Vec<u8>,
243) -> io::Result<()> {
244 if let Reply::Int(ms) = client.request_borrowed(&[b"PTTL", key])?
245 && ms > 0
246 {
247 let now = std::time::SystemTime::now()
248 .duration_since(std::time::UNIX_EPOCH)
249 .map_err(io::Error::other)?
250 .as_millis() as i64;
251 encode_command_borrowed(
252 frame,
253 &[b"PEXPIREAT", dst, (now + ms).to_string().as_bytes()],
254 );
255 }
256 Ok(())
257}
258
259pub struct ImportReport {
261 pub sent: u64,
263 pub errors: u64,
265 pub offset: u64,
267}
268
269pub fn run_import(
274 client: &mut RespClient,
275 src: &Path,
276 resume: bool,
277 strict: bool,
278) -> io::Result<ImportReport> {
279 let progress_path = src.with_extension("progress");
280 let mut start = 0u64;
281 if resume && let Ok(text) = std::fs::read_to_string(&progress_path) {
282 start = text.trim().parse().unwrap_or(0);
283 }
284 let mut f = File::open(src)?;
285 f.seek(SeekFrom::Start(start))?;
286 let mut pending: Vec<u8> = Vec::with_capacity(1 << 20);
287 let mut report = ImportReport { sent: 0, errors: 0, offset: start };
288 let mut chunk = vec![0u8; 1 << 20];
289 let mut batch_bytes = 0usize;
290 let mut batch_cmds = 0usize;
291 let mut progress = OpenOptions::new().create(true).truncate(false).write(true).open(&progress_path)?;
292 loop {
293 let n = f.read(&mut chunk)?;
294 if n == 0 {
295 break;
296 }
297 pending.extend_from_slice(&chunk[..n]);
298 while let Some(used) = command_len(&pending[batch_bytes..]) {
300 batch_bytes += used;
301 batch_cmds += 1;
302 if batch_cmds == PIPELINE {
303 flush_batch(client, &pending[..batch_bytes], batch_cmds, strict, &mut report)?;
304 pending.drain(..batch_bytes);
305 write_progress(&mut progress, report.offset)?;
306 batch_bytes = 0;
307 batch_cmds = 0;
308 }
309 }
310 }
311 if batch_cmds > 0 {
312 flush_batch(client, &pending[..batch_bytes], batch_cmds, strict, &mut report)?;
313 write_progress(&mut progress, report.offset)?;
314 }
315 Ok(report)
316}
317
318fn flush_batch(
319 client: &mut RespClient,
320 raw: &[u8],
321 n: usize,
322 strict: bool,
323 report: &mut ImportReport,
324) -> io::Result<()> {
325 let replies = client.pipeline_raw(raw, n)?;
326 for r in replies {
327 if let Reply::Error(e) = r {
328 report.errors += 1;
329 if strict {
330 return Err(io::Error::new(
331 io::ErrorKind::InvalidData,
332 format!("server error (strict): {}", String::from_utf8_lossy(&e)),
333 ));
334 }
335 } else {
336 report.sent += 1;
337 }
338 }
339 report.offset += raw.len() as u64;
340 Ok(())
341}
342
343fn write_progress(f: &mut File, offset: u64) -> io::Result<()> {
344 f.set_len(0)?;
345 f.seek(SeekFrom::Start(0))?;
346 f.write_all(offset.to_string().as_bytes())?;
347 f.sync_data()
348}
349
350pub(crate) fn command_len_pub(b: &[u8]) -> Option<usize> {
352 command_len(b)
353}
354
355fn command_len(b: &[u8]) -> Option<usize> {
357 let mut pos = 0usize;
358 let line = take_line(b, &mut pos)?;
359 if line.first() != Some(&b'*') {
360 return None;
361 }
362 let n: usize = std::str::from_utf8(&line[1..]).ok()?.trim().parse().ok()?;
363 for _ in 0..n {
364 let hdr = take_line(b, &mut pos)?;
365 if hdr.first() != Some(&b'$') {
366 return None;
367 }
368 let len: usize = std::str::from_utf8(&hdr[1..]).ok()?.trim().parse().ok()?;
369 if b.len() < pos + len + 2 {
370 return None;
371 }
372 pos += len + 2;
373 }
374 Some(pos)
375}
376
377fn take_line<'b>(b: &'b [u8], pos: &mut usize) -> Option<&'b [u8]> {
378 let rest = &b[*pos..];
379 let idx = rest.windows(2).position(|w| w == b"\r\n")?;
380 let line = &rest[..idx];
381 *pos += idx + 2;
382 Some(line)
383}