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 fn run_export(
25 client: &mut RespClient,
26 prefix: Option<&[u8]>,
27 out_path: &Path,
28) -> io::Result<u64> {
29 let mut out = BufWriter::new(File::create(out_path)?);
30 let mut cursor: Vec<u8> = b"0".to_vec();
31 let mut pattern = prefix.unwrap_or_default().to_vec();
32 pattern.push(b'*');
33 let mut n = 0u64;
34 loop {
35 let reply = client.request_borrowed(&[b"SCAN", &cursor, b"MATCH", &pattern, b"COUNT", b"512"])?;
36 let Reply::Array(items) = reply else {
37 return Err(io::Error::new(io::ErrorKind::InvalidData, "SCAN reply shape"));
38 };
39 let (Some(Reply::Bulk(next)), Some(Reply::Array(keys))) = (items.first(), items.get(1))
40 else {
41 return Err(io::Error::new(io::ErrorKind::InvalidData, "SCAN reply shape"));
42 };
43 let next = next.clone();
44 for k in keys {
45 let Reply::Bulk(key) = k else { continue };
46 let key = key.clone();
47 if export_key(client, &key, &mut out)? {
48 n += 1;
49 }
50 }
51 cursor = next;
52 if cursor == b"0" {
53 break;
54 }
55 }
56 out.flush()?;
57 Ok(n)
58}
59
60fn export_key(client: &mut RespClient, key: &[u8], out: &mut impl Write) -> io::Result<bool> {
63 match rebuild_frames(client, key, key)? {
64 Some(frame) => {
65 out.write_all(&frame)?;
66 Ok(true)
67 }
68 None => Ok(false),
69 }
70}
71
72pub(crate) fn rebuild_frames(
76 client: &mut RespClient,
77 key: &[u8],
78 dst: &[u8],
79) -> io::Result<Option<Vec<u8>>> {
80 let ty = match client.request_borrowed(&[b"TYPE", key])? {
81 Reply::Simple(t) => t,
82 _ => return Ok(None),
83 };
84 let mut frame = Vec::new();
85 encode_command_borrowed(&mut frame, &[b"DEL", dst]);
88 match ty.as_slice() {
89 b"string" => {
90 let Reply::Bulk(v) = client.request_borrowed(&[b"GET", key])? else {
91 return Ok(None);
92 };
93 encode_command_borrowed(&mut frame, &[b"SET", dst, &v]);
94 }
95 b"hash" => {
96 let Some(items) = fetch_bulks(client, &[b"HGETALL", key])? else {
97 return Ok(None);
98 };
99 encode_multi(&mut frame, b"HSET", dst, &items);
100 }
101 b"list" => {
102 let Some(vals) = fetch_bulks(client, &[b"LRANGE", key, b"0", b"-1"])? else {
103 return Ok(None);
104 };
105 encode_multi(&mut frame, b"RPUSH", dst, &vals);
106 }
107 b"set" => {
108 let Some(ms) = fetch_bulks(client, &[b"SMEMBERS", key])? else {
109 return Ok(None);
110 };
111 encode_multi(&mut frame, b"SADD", dst, &ms);
112 }
113 b"zset" => {
114 let zrange: &[&[u8]] = &[b"ZRANGE", key, b"0", b"-1", b"WITHSCORES"];
115 let Some(flat) = fetch_bulks(client, zrange)? else {
116 return Ok(None);
117 };
118 encode_zadd(&mut frame, dst, &flat);
119 }
120 _ => return Ok(None), }
122 append_ttl_frame(client, key, dst, &mut frame)?;
123 Ok(Some(frame))
124}
125
126fn fetch_bulks(client: &mut RespClient, cmd: &[&[u8]]) -> io::Result<Option<Vec<Vec<u8>>>> {
130 let Reply::Array(items) = client.request_borrowed(cmd)? else {
131 return Ok(None);
132 };
133 if items.is_empty() {
134 return Ok(None);
135 }
136 Ok(Some(
137 items
138 .into_iter()
139 .filter_map(|r| if let Reply::Bulk(b) = r { Some(b) } else { None })
140 .collect(),
141 ))
142}
143
144fn encode_multi(frame: &mut Vec<u8>, verb: &[u8], dst: &[u8], vals: &[Vec<u8>]) {
146 let mut argv: Vec<&[u8]> = vec![verb, dst];
147 argv.extend(vals.iter().map(Vec::as_slice));
148 encode_command_borrowed(frame, &argv);
149}
150
151fn encode_zadd(frame: &mut Vec<u8>, dst: &[u8], flat: &[Vec<u8>]) {
154 let mut argv: Vec<&[u8]> = vec![b"ZADD", dst];
155 for pair in flat.chunks(2) {
156 if pair.len() == 2 {
157 argv.push(&pair[1]);
158 argv.push(&pair[0]);
159 }
160 }
161 encode_command_borrowed(frame, &argv);
162}
163
164fn append_ttl_frame(
166 client: &mut RespClient,
167 key: &[u8],
168 dst: &[u8],
169 frame: &mut Vec<u8>,
170) -> io::Result<()> {
171 if let Reply::Int(ms) = client.request_borrowed(&[b"PTTL", key])?
172 && ms > 0
173 {
174 let now = std::time::SystemTime::now()
175 .duration_since(std::time::UNIX_EPOCH)
176 .map_err(io::Error::other)?
177 .as_millis() as i64;
178 encode_command_borrowed(
179 frame,
180 &[b"PEXPIREAT", dst, (now + ms).to_string().as_bytes()],
181 );
182 }
183 Ok(())
184}
185
186pub struct ImportReport {
188 pub sent: u64,
190 pub errors: u64,
192 pub offset: u64,
194}
195
196pub fn run_import(
201 client: &mut RespClient,
202 src: &Path,
203 resume: bool,
204 strict: bool,
205) -> io::Result<ImportReport> {
206 let progress_path = src.with_extension("progress");
207 let mut start = 0u64;
208 if resume && let Ok(text) = std::fs::read_to_string(&progress_path) {
209 start = text.trim().parse().unwrap_or(0);
210 }
211 let mut f = File::open(src)?;
212 f.seek(SeekFrom::Start(start))?;
213 let mut pending: Vec<u8> = Vec::with_capacity(1 << 20);
214 let mut report = ImportReport { sent: 0, errors: 0, offset: start };
215 let mut chunk = vec![0u8; 1 << 20];
216 let mut batch_bytes = 0usize;
217 let mut batch_cmds = 0usize;
218 let mut progress = OpenOptions::new().create(true).truncate(false).write(true).open(&progress_path)?;
219 loop {
220 let n = f.read(&mut chunk)?;
221 if n == 0 {
222 break;
223 }
224 pending.extend_from_slice(&chunk[..n]);
225 while let Some(used) = command_len(&pending[batch_bytes..]) {
227 batch_bytes += used;
228 batch_cmds += 1;
229 if batch_cmds == PIPELINE {
230 flush_batch(client, &pending[..batch_bytes], batch_cmds, strict, &mut report)?;
231 pending.drain(..batch_bytes);
232 write_progress(&mut progress, report.offset)?;
233 batch_bytes = 0;
234 batch_cmds = 0;
235 }
236 }
237 }
238 if batch_cmds > 0 {
239 flush_batch(client, &pending[..batch_bytes], batch_cmds, strict, &mut report)?;
240 write_progress(&mut progress, report.offset)?;
241 }
242 Ok(report)
243}
244
245fn flush_batch(
246 client: &mut RespClient,
247 raw: &[u8],
248 n: usize,
249 strict: bool,
250 report: &mut ImportReport,
251) -> io::Result<()> {
252 let replies = client.pipeline_raw(raw, n)?;
253 for r in replies {
254 if let Reply::Error(e) = r {
255 report.errors += 1;
256 if strict {
257 return Err(io::Error::new(
258 io::ErrorKind::InvalidData,
259 format!("server error (strict): {}", String::from_utf8_lossy(&e)),
260 ));
261 }
262 } else {
263 report.sent += 1;
264 }
265 }
266 report.offset += raw.len() as u64;
267 Ok(())
268}
269
270fn write_progress(f: &mut File, offset: u64) -> io::Result<()> {
271 f.set_len(0)?;
272 f.seek(SeekFrom::Start(0))?;
273 f.write_all(offset.to_string().as_bytes())?;
274 f.sync_data()
275}
276
277pub(crate) fn command_len_pub(b: &[u8]) -> Option<usize> {
279 command_len(b)
280}
281
282fn command_len(b: &[u8]) -> Option<usize> {
284 let mut pos = 0usize;
285 let line = take_line(b, &mut pos)?;
286 if line.first() != Some(&b'*') {
287 return None;
288 }
289 let n: usize = std::str::from_utf8(&line[1..]).ok()?.trim().parse().ok()?;
290 for _ in 0..n {
291 let hdr = take_line(b, &mut pos)?;
292 if hdr.first() != Some(&b'$') {
293 return None;
294 }
295 let len: usize = std::str::from_utf8(&hdr[1..]).ok()?.trim().parse().ok()?;
296 if b.len() < pos + len + 2 {
297 return None;
298 }
299 pos += len + 2;
300 }
301 Some(pos)
302}
303
304fn take_line<'b>(b: &'b [u8], pos: &mut usize) -> Option<&'b [u8]> {
305 let rest = &b[*pos..];
306 let idx = rest.windows(2).position(|w| w == b"\r\n")?;
307 let line = &rest[..idx];
308 *pos += idx + 2;
309 Some(line)
310}