|
|
@@ -89,6 +89,11 @@ pub(crate) async fn build_composed_dir_on_node(
|
|
|
/// Resolve a source repo's snapshot on the node and symlink its files into
|
|
|
/// the composed dir. Symlink targets are container-side absolute paths
|
|
|
/// (`/root/.cache/huggingface/...`) so they resolve inside the container.
|
|
|
+///
|
|
|
+/// With no `glob`, the ENTIRE snapshot hierarchy is symlinked recursively
|
|
|
+/// (every file in every subdir, real dirs) — so a later source can overlay
|
|
|
+/// individual files at any path. With a `glob`, only matching top-level (or
|
|
|
+/// explicit-subdir) entries are symlinked.
|
|
|
async fn symlink_source(
|
|
|
config: &crate::config::Config,
|
|
|
node: &crate::config::Node,
|
|
|
@@ -98,22 +103,33 @@ async fn symlink_source(
|
|
|
) -> Result<()> {
|
|
|
let model_dir = crate::config::model_cache_dir_name(&source.repo);
|
|
|
let refs_path = format!("{}/hub/{}/refs/main", cache_str, model_dir);
|
|
|
- let glob = source.glob.as_deref().unwrap_or("*");
|
|
|
+ let glob = source.glob.as_deref();
|
|
|
|
|
|
// Validate the glob: only allow glob-safe characters to prevent shell
|
|
|
// injection (the glob is inserted raw into the shell `for` statement).
|
|
|
- if !glob
|
|
|
- .chars()
|
|
|
- .all(|c| c.is_ascii_alphanumeric() || "*?._-/".contains(c))
|
|
|
+ if let Some(g) = glob
|
|
|
+ && !g
|
|
|
+ .chars()
|
|
|
+ .all(|c| c.is_ascii_alphanumeric() || "*?._-/".contains(c))
|
|
|
{
|
|
|
anyhow::bail!(
|
|
|
"composed_model source '{}': glob '{}' contains invalid characters \
|
|
|
(allowed: alphanumeric, * ? . _ - /)",
|
|
|
source.repo,
|
|
|
- glob
|
|
|
+ g
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+ // Enumerate: no glob → recursive `find` (whole hierarchy, every file in
|
|
|
+ // every subdir); glob → shell glob match (one level, `/` allowed for a
|
|
|
+ // specific subdir). Both feed the same per-file body below.
|
|
|
+ let enumerate: String = match glob {
|
|
|
+ None => String::from(
|
|
|
+ "find \"$snap\" -mindepth 1 \\( -type l -o -type f \\) | while IFS= read -r f; do",
|
|
|
+ ),
|
|
|
+ Some(g) => format!("for f in \"$snap\"/{}; do", g),
|
|
|
+ };
|
|
|
+
|
|
|
// One SSH round-trip: resolve refs/main, then symlink each matching file.
|
|
|
// The symlink TARGET is the CONTAINER-side path (/root/.cache/huggingface/...)
|
|
|
// not the host-side path — the link is dangling on the host but resolves
|
|
|
@@ -127,14 +143,16 @@ async fn symlink_source(
|
|
|
//
|
|
|
// Relative paths are preserved: a glob like `grids/*.json` creates
|
|
|
// `$host/grids/L10.json` (with the subdir), and the symlink target is
|
|
|
- // `$csnap/grids/L10.json` (matching structure). `mkdir -p` creates
|
|
|
- // parent dirs as needed.
|
|
|
+ // `$csnap/grids/L10.json` (matching structure). With no glob, `find`
|
|
|
+ // recurses so `mtp-draft/foo.bin` etc. are symlinked individually.
|
|
|
+ // `mkdir -p` creates parent dirs as needed (real dirs, not symlinks —
|
|
|
+ // so later sources can overlay files at any path).
|
|
|
let cmd = format!(
|
|
|
"ref=\"$(cat {refs_q} 2>/dev/null)\" && test -n \"$ref\" && \
|
|
|
snap=\"$(printf '%s/hub/%s/snapshots/%s' {cache_q} {model_q} \"$ref\")\" && \
|
|
|
test -d \"$snap\" && \
|
|
|
csnap=\"$(printf '/root/.cache/huggingface/hub/%s/snapshots/%s' {model_q} \"$ref\")\" && \
|
|
|
- for f in \"$snap\"/{glob}; do \
|
|
|
+ {enumerate} \
|
|
|
[ -e \"$f\" ] || [ -L \"$f\" ] || continue; \
|
|
|
rel=\"${{f#\"$snap\"/}}\"; \
|
|
|
parent=\"$(dirname {host_q}/\"$rel\")\"; \
|
|
|
@@ -145,6 +163,7 @@ async fn symlink_source(
|
|
|
cache_q = shq(cache_str),
|
|
|
model_q = shq(&model_dir),
|
|
|
host_q = shq(host_path),
|
|
|
+ enumerate = enumerate,
|
|
|
);
|
|
|
|
|
|
run_ssh(config, node, cmd, false).await.with_context(|| {
|