This commit is contained in:
sergio
2026-05-10 21:58:16 +00:00
parent 3d55f189c0
commit c22d2480b9
36 changed files with 5158 additions and 363 deletions
@@ -3,10 +3,11 @@ name = "yahweh-provider-fs"
version = { workspace = true }
edition = { workspace = true }
license = { workspace = true }
description = "DataProvider de filesystem local."
description = "DataProvider de filesystem local con discernimiento de contenido (shipote-discern)."
[dependencies]
yahweh-core = { workspace = true }
async-trait = { workspace = true }
tokio = { workspace = true }
notify = { workspace = true }
shipote-discern = { path = "../../../../../modules/shipote/shipote-discern" }
@@ -3,16 +3,45 @@
//! `std::fs::read_dir` y leyendo archivos a `Vec<u8>` via `tokio::io`.
use async_trait::async_trait;
use shipote_discern::{DiscernPipeline, Hint};
use std::fs;
use std::io::Cursor;
use std::io::{Cursor, Read};
use std::path::Path;
use std::pin::Pin;
use std::sync::Arc;
use tokio::io::{AsyncRead, AsyncWrite};
use yahweh_core::{DataProvider, DisplayType, EntityNode};
pub const PROVIDER_ID: &str = "local_fs";
pub struct FileDataProvider;
/// Bytes que samplea el discerner por archivo. 4 KiB cubre headers de
/// formatos comunes (PNG, ELF, JSON/TOML hasta una clave de profundidad
/// razonable) sin saturar I/O al expandir un directorio.
const DISCERN_SAMPLE_BYTES: usize = 4096;
/// Tamaño máximo de archivo que sampleamos. Archivos más grandes se
/// discernen igual via los primeros 4 KiB: el `seek/read` siempre lee
/// head, y el costo es O(SAMPLE) sin importar el size total.
/// Mantenemos esta constante por documentación; no se usa para skipear.
const _DISCERN_SAMPLE_DOC: () = ();
pub struct FileDataProvider {
discerner: Arc<DiscernPipeline>,
}
impl FileDataProvider {
pub fn new() -> Self {
Self {
discerner: Arc::new(DiscernPipeline::default_pipeline()),
}
}
}
impl Default for FileDataProvider {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl DataProvider for FileDataProvider {
@@ -32,17 +61,21 @@ impl DataProvider for FileDataProvider {
.unwrap_or_default()
.to_string_lossy()
.into_owned();
let display_type = if path.is_dir() {
DisplayType::Folder
let is_dir = path.is_dir();
let display_type = if is_dir { DisplayType::Folder } else { DisplayType::File };
// Discernimos sólo archivos. Folders no tienen MIME útil.
let mime_type = if is_dir {
None
} else {
DisplayType::File
discern_head(&path, &self.discerner)
};
children.push(EntityNode {
id: path.to_string_lossy().into_owned(),
name,
display_type,
mime_type: None,
mime_type,
});
}
}
@@ -65,3 +98,22 @@ impl DataProvider for FileDataProvider {
Err("Escritura en streaming no implementada para FS".to_string())
}
}
/// Lee el head del archivo y lo pasa por el DiscernPipeline. Devuelve el
/// MIME detectado (si alguno) o `None` si no hubo match.
///
/// Sync intencional: estamos dentro del runtime que ya es async, pero la
/// lectura es de tamaño fijo (4 KiB) y va a page cache; el costo de
/// `tokio::fs` no compensaría para esto.
fn discern_head(path: &Path, discerner: &DiscernPipeline) -> Option<String> {
let mut buf = vec![0u8; DISCERN_SAMPLE_BYTES];
let mut f = fs::File::open(path).ok()?;
let n = f.read(&mut buf).ok()?;
buf.truncate(n);
let path_str = path.to_str();
let hint = Hint {
path: path_str,
size_total: None,
};
discerner.discern(&buf, &hint).and_then(|d| d.mime)
}