use std::path::{Path, PathBuf};
use bytes::Bytes;
use globset::{Glob, GlobSetBuilder};
use super::error::DrainError;
use super::manifest::SkipReason;
use super::pipeline::{StreamOutcome, stream_file};
pub use super::pipeline::Level;
pub const DEFAULT_MAX_FILE_BYTES: u64 = 4 * 1024 * 1024 * 1024;
pub const DEFAULT_MAX_WIRE_BYTES: u64 = 64 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct CollectLimits {
pub max_file_bytes: u64,
pub max_wire_bytes: u64,
}
impl Default for CollectLimits {
fn default() -> Self {
Self {
max_file_bytes: DEFAULT_MAX_FILE_BYTES,
max_wire_bytes: DEFAULT_MAX_WIRE_BYTES,
}
}
}
impl CollectLimits {
pub fn new(max_file_bytes: u64, max_wire_bytes: u64) -> Self {
Self {
max_file_bytes,
max_wire_bytes,
}
}
}
#[derive(Debug, Clone)]
pub struct LogSource {
pub crate_name: String,
pub root: PathBuf,
pub include: Vec<String>,
pub level_filter: Option<Level>,
}
#[derive(Debug, Clone)]
pub struct CollectedFile {
pub relative_key: String,
pub body: Bytes,
pub sha256_plaintext: String,
pub plaintext_len: u64,
pub mtime_unix: i64,
pub source_path: PathBuf,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct OversizeFile {
pub path: PathBuf,
pub relative_key: String,
pub size: u64,
pub mtime_unix: i64,
pub reason: SkipReason,
}
#[derive(Debug, Default)]
pub struct Collected {
pub files: Vec<CollectedFile>,
pub oversize: Vec<OversizeFile>,
pub errors: Vec<(PathBuf, String)>,
}
pub fn collect(
sources: &[LogSource],
secrets: &[String],
limits: CollectLimits,
) -> Result<Collected, DrainError> {
let mut out = Collected::default();
for source in sources {
let mut builder = GlobSetBuilder::new();
for pattern in &source.include {
let glob = Glob::new(pattern).map_err(|e| DrainError::Uri {
uri: pattern.clone(),
reason: format!("invalid include glob: {e}"),
})?;
builder.add(glob);
}
let globs = builder.build().map_err(|e| DrainError::Uri {
uri: source.include.join(","),
reason: format!("could not compile include globs: {e}"),
})?;
for entry in walkdir::WalkDir::new(&source.root)
.follow_links(false)
.into_iter()
.filter_map(Result::ok)
{
if !entry.file_type().is_file() {
continue;
}
let path = entry.path();
let Ok(relative) = path.strip_prefix(&source.root) else {
continue;
};
if !globs.is_match(relative) {
continue;
}
process_file(source, path, relative, secrets, limits, &mut out);
}
}
Ok(out)
}
fn process_file(
source: &LogSource,
path: &Path,
relative: &Path,
secrets: &[String],
limits: CollectLimits,
out: &mut Collected,
) {
let metadata = match std::fs::metadata(path) {
Ok(m) => m,
Err(e) => {
out.errors.push((path.to_path_buf(), e.to_string()));
return;
}
};
let size = metadata.len();
let mtime_unix = mtime_seconds(&metadata);
let relative_key = format!("{}/{}", source.crate_name, relative.to_string_lossy());
let reason = if size > limits.max_file_bytes {
SkipReason::SourceTooLarge
} else {
match stream_file(path, source.level_filter, secrets, limits.max_wire_bytes) {
Ok(StreamOutcome::Body {
body,
sha256_plaintext,
}) => {
out.files.push(CollectedFile {
relative_key,
body,
sha256_plaintext,
plaintext_len: size,
mtime_unix,
source_path: path.to_path_buf(),
});
return;
}
Ok(StreamOutcome::CompressedTooLarge) => SkipReason::CompressedTooLarge,
Err(e) => {
out.errors.push((path.to_path_buf(), e.to_string()));
return;
}
}
};
out.oversize.push(OversizeFile {
path: path.to_path_buf(),
relative_key,
size,
mtime_unix,
reason,
});
}
fn mtime_seconds(metadata: &std::fs::Metadata) -> i64 {
metadata
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map_or(0, |d| d.as_secs() as i64)
}