tale_ndjson/readers/
backseeking.rs1use std::fs::File;
6use std::io::{self, BufRead, BufReader, Read, Seek, Write};
7use std::path::PathBuf;
8use std::time::{Duration, Instant};
9
10use bytes::BytesMut;
11use miette::IntoDiagnostic;
12
13use super::FileProcessor;
14use crate::defaults::io::*;
15use crate::defaults::processing::*;
16use crate::errors::TaleError;
17use crate::{config, process_line, strip_line_ending};
18
19pub struct BackSeekingProcessor<'a> {
20 fpath: PathBuf,
21 initial_file_size: u64,
22 file: Option<File>,
23 outlock: io::StdoutLock<'a>,
24 buffer: BytesMut,
25 count: u16,
26}
27
28impl<'a> FileProcessor for BackSeekingProcessor<'a> {
30 fn process_lines<F>(&mut self, _line_processor: F) -> Result<(), TaleError>
31 where
32 F: FnMut(&str) -> Result<(), TaleError>,
33 {
34 let _temp_buffer = BytesMut::with_capacity(OUTPUT_BUFFER_CAPACITY);
35 let _temp_outlock = io::stdout().lock();
36
37 todo!("Refactor existing tail() method to support callback-based processing")
42 }
43
44 fn skip_lines(&mut self, _count: u64) -> Result<(), TaleError> {
45 todo!("Implement using existing offset logic")
48 }
49
50 fn file_size(&self) -> u64 {
51 self.initial_file_size
52 }
53
54 fn seek(&mut self, pos: io::SeekFrom) -> Result<u64, TaleError> {
55 if let Some(mut f) = self.file.as_ref() {
56 Ok(f.seek(pos).map_err(TaleError::from)?)
57 } else {
58 let mut file = File::open(&self.fpath).map_err(TaleError::from)?;
59 let actual = file.seek(pos).map_err(TaleError::from)?;
60 self.file = Some(file);
61 Ok(actual)
62 }
63 }
64
65 fn position(&self) -> u64 {
66 if let Some(mut f) = self.file.as_ref() {
67 f.stream_position().unwrap_or_default()
68 } else {
69 0
70 }
71 }
72}
73
74impl<'a> BackSeekingProcessor<'a> {
75 pub fn new(fpath: PathBuf) -> Self {
76 let file_size = if let Ok(mut file) = File::open(&fpath) {
78 file.seek(io::SeekFrom::End(0)).unwrap_or_default()
79 } else {
80 0
81 };
82
83 Self {
84 fpath,
85 initial_file_size: file_size,
86 file: None,
87 outlock: io::stdout().lock(),
88 buffer: BytesMut::with_capacity(OUTPUT_BUFFER_CAPACITY),
89 count: 0,
90 }
91 }
92
93 pub fn move_to_position(
100 &mut self,
101 offset: i64,
102 units: config::OffsetUnit,
103 tailing: bool,
104 ) -> Result<File, TaleError> {
105 let mut file = File::open(&self.fpath)?;
106 let file_size = file.seek(io::SeekFrom::End(0))?;
108 if file_size == 0 {
109 return Ok(file);
110 }
111
112 file.seek(io::SeekFrom::Start(0))?;
114
115 match units {
117 config::OffsetUnit::Lines => {
118 if offset > 0 {
119 file.seek(io::SeekFrom::Start(0))?;
122 } else if offset < 0 {
123 let start = self.move_n_lines_back(&mut file, (-offset) as u64)?;
125 file.seek(io::SeekFrom::Start(start))?;
126 } else if tailing {
127 file.seek(io::SeekFrom::End(0))?;
129 }
130 }
131 config::OffsetUnit::Bytes => {
132 if offset > 0 {
134 file.seek(io::SeekFrom::Start(offset as u64))?;
136 } else if offset < 0 {
137 file.seek(io::SeekFrom::End(offset))?;
139 } else if tailing {
140 file.seek(io::SeekFrom::End(0))?;
142 }
143 }
144 config::OffsetUnit::Blocks => {
145 if offset > 0 {
147 let byte_offset = (offset as u64) * BLOCK_SIZE;
148 file.seek(io::SeekFrom::Start(byte_offset))?;
149 } else if offset < 0 {
150 let byte_offset = offset * (BLOCK_SIZE as i64);
151 file.seek(io::SeekFrom::End(byte_offset))?;
152 } else if tailing {
153 file.seek(io::SeekFrom::End(0))?;
154 }
155 }
156 }
157
158 Ok(file)
159 }
160
161 fn move_n_lines_back(&mut self, file: &mut File, line_count: u64) -> Result<u64, TaleError> {
166 let file_size = file.seek(io::SeekFrom::End(0))?;
167 if file_size == 0 {
168 return Ok(0);
169 }
170
171 const BUFFER_SIZE: usize = 8192;
172 let mut buffer = vec![0u8; BUFFER_SIZE];
173 let mut lines_found = 0u64;
174
175 file.seek(io::SeekFrom::End(-1))?;
177 let mut last_byte = [0u8; 1];
178 file.read_exact(&mut last_byte)?;
179 let ends_with_newline = last_byte[0] == b'\n';
180
181 let target_newlines = if ends_with_newline { line_count } else { line_count - 1 };
185
186 let mut pos = file_size;
187
188 loop {
189 let chunk_size = std::cmp::min(BUFFER_SIZE as u64, pos) as usize;
191 if chunk_size == 0 {
192 return Ok(0);
194 }
195
196 pos -= chunk_size as u64;
198 file.seek(io::SeekFrom::Start(pos))?;
199 file.read_exact(&mut buffer[..chunk_size])?;
200
201 for (i, &byte) in buffer[..chunk_size].iter().enumerate().rev() {
203 if byte == b'\n' {
204 lines_found += 1;
205 if lines_found > target_newlines {
206 return Ok(pos + i as u64 + 1);
208 }
209 }
210 }
211
212 if pos == 0 {
215 return Ok(0);
216 }
217 }
218 }
219
220 pub fn process_line(&mut self, line: &str) -> Result<(), TaleError> {
222 process_line(line, &mut self.buffer, &mut self.outlock)
223 .map_err(|e| TaleError::from(std::io::Error::other(e.to_string())))?;
224 self.count += 1;
225 self.flush_if_needed()
226 }
227
228 pub fn flush_if_needed(&mut self) -> Result<(), TaleError> {
230 if self.count >= FLUSH_LINE_COUNT {
231 self.outlock.flush()?;
232 self.count = 0;
233 }
234 Ok(())
235 }
236
237 pub fn flush(&mut self) -> Result<(), TaleError> {
239 self.outlock.flush()?;
240 self.count = 0;
241 Ok(())
242 }
243
244 pub fn tail(&mut self) -> miette::Result<()> {
245 let tailing = config::tailing();
246 let offset_unit = config::offset_unit();
247 let offset = config::offset();
248
249 let file = self
250 .move_to_position(offset, offset_unit, tailing)
251 .map_err(miette::Report::from)?;
252 let mut reader = BufReader::new(file);
253
254 if offset > 0 && matches!(offset_unit, config::OffsetUnit::Lines) {
256 let consume_me = (&mut reader).lines().take(offset as usize);
257 let _count = consume_me.count();
259 };
260
261 let mut line = String::with_capacity(LINE_CAPACITY);
263 while reader.read_line(&mut line).into_diagnostic()? != 0 {
264 strip_line_ending(&mut line);
265 self.process_line(line.as_str()).map_err(miette::Report::from)?;
266 line.clear();
267 }
268 self.flush().map_err(miette::Report::from)?;
269
270 if !tailing {
271 return Ok(());
272 }
273
274 let mut last_flush = Instant::now();
276
277 let mut file = reader.into_inner();
279 let mut file_position = file.stream_position().into_diagnostic()?;
280
281 loop {
283 std::thread::sleep(Duration::from_millis(100));
284
285 let current_size = file.seek(io::SeekFrom::End(0)).into_diagnostic()?;
287 if current_size > file_position {
288 file.seek(io::SeekFrom::Start(file_position)).into_diagnostic()?;
290 let mut tail_reader = BufReader::new(&file);
291
292 match tail_reader.read_line(&mut line).into_diagnostic()? {
293 0 => {
294 continue;
296 }
297 _ => {
298 strip_line_ending(&mut line);
299 process_line(&line, &mut self.buffer, &mut self.outlock)?;
301 if last_flush.elapsed() >= TAIL_FLUSH_INTERVAL {
302 self.outlock.flush().into_diagnostic()?;
303 last_flush = Instant::now();
304 }
305
306 line.clear();
307 self.buffer.clear();
308 }
309 }
310
311 file_position = file.stream_position().into_diagnostic()?;
313 }
314 }
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn seeking_backwards() {
324 use std::io::{Read, Seek, Write};
325
326 use tempfile::NamedTempFile;
327
328 let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
330 let content = "line1\nline2\nline3\nline4\nline5\n";
331 temp_file
332 .write_all(content.as_bytes())
333 .expect("Failed to write to temp file");
334
335 let pathbuf = PathBuf::from(temp_file.path());
336 let mut processor = BackSeekingProcessor::new(pathbuf);
337 let mut file = File::open(temp_file.path()).expect("Failed to open temp file");
338
339 let pos = processor
341 .move_n_lines_back(&mut file, 2)
342 .expect("Failed to find position");
343 file.seek(io::SeekFrom::Start(pos)).expect("Failed to seek");
344
345 let mut remaining = String::new();
346 file.read_to_string(&mut remaining).expect("Failed to read remaining");
347 assert_eq!(remaining, "line4\nline5\n");
348
349 let pos = processor
351 .move_n_lines_back(&mut file, 1)
352 .expect("Failed to find position");
353 file.seek(io::SeekFrom::Start(pos)).expect("Failed to seek");
354
355 let mut remaining = String::new();
356 file.read_to_string(&mut remaining).expect("Failed to read remaining");
357 assert_eq!(remaining, "line5\n");
358
359 let pos = processor
361 .move_n_lines_back(&mut file, 10)
362 .expect("Failed to find position");
363 assert_eq!(pos, 0);
364 }
365
366 #[test]
367 fn seeking_in_empty() {
368 use tempfile::NamedTempFile;
369 let temp_file = NamedTempFile::new().expect("Failed to create temp file");
370 let pathbuf = PathBuf::from(temp_file.path());
371 let mut processor = BackSeekingProcessor::new(pathbuf);
372 let mut file = File::open(temp_file.path()).expect("Failed to open temp file");
373
374 let pos = processor
375 .move_n_lines_back(&mut file, 5)
376 .expect("Failed to find position");
377 assert_eq!(pos, 0);
378 }
379
380 #[test]
381 fn good_circular_buffer_byte_logic() {
382 let input_data = b"0123456789abcdefghij";
384 let buffer_size = 10;
385 let mut circular_buffer = vec![0u8; buffer_size];
386 let mut pos = 0usize;
387
388 for &byte in input_data {
390 circular_buffer[pos % buffer_size] = byte;
391 pos += 1;
392 }
393
394 let _total_read = input_data.len() as u64;
396 let _bytes_to_show = buffer_size as u64;
397 let start_pos = pos % buffer_size;
398
399 let mut result = Vec::with_capacity(buffer_size);
401 for i in 0..buffer_size {
402 result.push(circular_buffer[(start_pos + i) % buffer_size]);
403 }
404
405 assert_eq!(result, b"abcdefghij");
406 }
407
408 #[test]
409 fn circular_buffer_partial_fill_works() {
410 let input_data = b"hello";
412 let buffer_size = 10;
413 let mut circular_buffer = vec![0u8; buffer_size];
414
415 for (i, &byte) in input_data.iter().enumerate() {
417 circular_buffer[i] = byte;
418 }
419
420 let bytes_to_output = input_data.len();
421 let result = &circular_buffer[..bytes_to_output];
422
423 assert_eq!(result, b"hello");
424 }
425
426 #[test]
427 fn circular_line_logic() {
428 use std::collections::VecDeque;
429
430 let lines_to_keep = 3;
432 let mut line_buffer: VecDeque<String> = VecDeque::with_capacity(lines_to_keep);
433
434 let input_lines = vec!["line1", "line2", "line3", "line4", "line5"];
435
436 for line in input_lines {
437 if line_buffer.len() >= lines_to_keep {
438 line_buffer.pop_front();
439 }
440 line_buffer.push_back(line.to_string());
441 }
442
443 let result: Vec<String> = line_buffer.into_iter().collect();
445 assert_eq!(result, vec!["line3", "line4", "line5"]);
446 }
447
448 #[test]
449 fn handles_overshoots_correctly() {
450 let overshoot = b"partial line\ncomplete line\nanother";
451 let overshoot_str = String::from_utf8_lossy(overshoot);
452 let mut remaining = overshoot_str.as_ref();
453 let mut complete_lines = Vec::new();
454
455 while let Some(pos) = remaining.find('\n') {
456 complete_lines.push(&remaining[..pos]);
457 remaining = &remaining[pos + 1..];
458 }
459
460 assert_eq!(complete_lines, vec!["partial line", "complete line"]);
461 assert_eq!(remaining, "another");
462 }
463}