pull: add --repository to pull from external flat repositories
Add a --repository flag taking the full suite URL of an external flat repository (e.g. https://pkg.noctalia.dev/deb/resolute/), i.e. one with no dists/ hierarchy, like apt's exact-path suites ('Suites: resolute/'). The suite name is read from the root Release file (Codename/Suite), the sources index is fetched from the repository root as Sources.xz/gz/plain, and package files are resolved against the URL root, ignoring the stanza Directory field like apt does. As with PPAs, the stanza Vcs-Git is never used for external repositories, so the source always comes from the repository itself. Also make the sources index parser detect compression by magic bytes (gz/xz/plain) instead of assuming gzip, and fix extraction of archives with './'-prefixed entries, which previously aborted and are now extracted in place instead of being relocated.
This commit is contained in:
+79
-44
@@ -153,13 +153,23 @@ fn copy_file_times(src: &Path, dest: &Path) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Result of extracting an archive
|
||||
struct ExtractedArchive {
|
||||
/// Paths of the extracted files
|
||||
files: Vec<String>,
|
||||
/// The archive used './'-prefixed entries: its contents were extracted
|
||||
/// directly into the destination directory, and there is no
|
||||
/// 'package-version/' top-level directory to relocate
|
||||
in_place: bool,
|
||||
}
|
||||
|
||||
/// Helper function to extract tar archive with progress tracking
|
||||
fn extract_tar_archive<D, F>(
|
||||
file_path: &Path,
|
||||
dest: &Path,
|
||||
progress: ProgressCallback<'_>,
|
||||
decoder_factory: F,
|
||||
) -> Result<Vec<String>, Box<dyn Error>>
|
||||
) -> Result<ExtractedArchive, Box<dyn Error>>
|
||||
where
|
||||
D: std::io::Read,
|
||||
F: Fn(File) -> D,
|
||||
@@ -178,11 +188,28 @@ where
|
||||
let mut archive = Archive::new(decoder);
|
||||
|
||||
let mut extracted_files = Vec::new();
|
||||
let mut in_place = false;
|
||||
|
||||
for entry in archive.entries()? {
|
||||
let mut entry = entry?;
|
||||
let path = entry.path()?.to_path_buf();
|
||||
let dest_path = dest.join(&path);
|
||||
|
||||
// Archives built with './'-prefixed entries (common in third-party
|
||||
// repositories) target the destination directory itself, with no
|
||||
// 'package-version/' wrapper; skip their root entry, and remember
|
||||
// the layout for the caller
|
||||
let relative = if path.to_string_lossy().starts_with("./") {
|
||||
in_place = true;
|
||||
path.strip_prefix("./").unwrap_or(&path).to_path_buf()
|
||||
} else {
|
||||
path
|
||||
};
|
||||
|
||||
if relative.as_os_str().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dest_path = dest.join(&relative);
|
||||
|
||||
// Create parent directories if needed
|
||||
if let Some(parent) = dest_path.parent() {
|
||||
@@ -201,14 +228,17 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
Ok(extracted_files)
|
||||
Ok(ExtractedArchive {
|
||||
files: extracted_files,
|
||||
in_place,
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_archive(
|
||||
path: &Path,
|
||||
dest: &Path,
|
||||
progress: ProgressCallback<'_>,
|
||||
) -> Result<Vec<String>, Box<dyn Error>> {
|
||||
) -> Result<ExtractedArchive, Box<dyn Error>> {
|
||||
let filename = path.file_name().unwrap().to_string_lossy();
|
||||
|
||||
if filename.ends_with(".tar.gz") || filename.ends_with(".tgz") {
|
||||
@@ -465,47 +495,52 @@ async fn fetch_archive_sources(
|
||||
continue;
|
||||
}
|
||||
|
||||
// List root directories extracted and use the first one as the source directory
|
||||
debug!("Root directories extracted:");
|
||||
let mut source_dir: Option<PathBuf> = None;
|
||||
for file in &extracted {
|
||||
let path = Path::new(file);
|
||||
// Check if this is a directory and is at the archive root level
|
||||
// (i.e., the path relative to extract_dir has no parent components)
|
||||
if let Ok(relative_path) = path.strip_prefix(&extract_dir)
|
||||
&& relative_path.components().count() == 1
|
||||
&& path.is_dir()
|
||||
{
|
||||
debug!("- {}", relative_path.file_name().unwrap().to_string_lossy());
|
||||
// Use the first directory found as the source
|
||||
if source_dir.is_none() {
|
||||
source_dir = Some(path.to_path_buf());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the extracted directory as the source, assuming there is only one
|
||||
if let Some(src_dir) = source_dir {
|
||||
let target_dir = package_dir.join(&info.stanza.package);
|
||||
|
||||
if target_dir.exists() {
|
||||
// Target exists, we need to merge contents
|
||||
for sub_entry in std::fs::read_dir(&src_dir)? {
|
||||
let sub_entry = sub_entry?;
|
||||
let sub_path = sub_entry.path();
|
||||
let target_path = target_dir.join(sub_entry.file_name());
|
||||
if sub_path.is_dir() {
|
||||
std::fs::create_dir_all(&target_path)?;
|
||||
// Recursively copy directory contents
|
||||
copy_dir_all(&sub_path, &target_path)?;
|
||||
} else {
|
||||
std::fs::copy(&sub_path, &target_path)?;
|
||||
copy_file_times(&sub_path, &target_path)?;
|
||||
// Archives with './'-prefixed entries are already laid out
|
||||
// directly in the package directory; only 'package-version/'
|
||||
// style ones need their contents relocated
|
||||
if !extracted.in_place {
|
||||
// List root directories extracted and use the first one as the source directory
|
||||
debug!("Root directories extracted:");
|
||||
let mut source_dir: Option<PathBuf> = None;
|
||||
for file in &extracted.files {
|
||||
let path = Path::new(file);
|
||||
// Check if this is a directory and is at the archive root level
|
||||
// (i.e., the path relative to extract_dir has no parent components)
|
||||
if let Ok(relative_path) = path.strip_prefix(&extract_dir)
|
||||
&& relative_path.components().count() == 1
|
||||
&& path.is_dir()
|
||||
{
|
||||
debug!("- {}", relative_path.file_name().unwrap().to_string_lossy());
|
||||
// Use the first directory found as the source
|
||||
if source_dir.is_none() {
|
||||
source_dir = Some(path.to_path_buf());
|
||||
}
|
||||
}
|
||||
std::fs::remove_dir_all(&src_dir)?;
|
||||
} else {
|
||||
std::fs::rename(&src_dir, &target_dir)?;
|
||||
}
|
||||
|
||||
// Use the extracted directory as the source, assuming there is only one
|
||||
if let Some(src_dir) = source_dir {
|
||||
let target_dir = package_dir.join(&info.stanza.package);
|
||||
|
||||
if target_dir.exists() {
|
||||
// Target exists, we need to merge contents
|
||||
for sub_entry in std::fs::read_dir(&src_dir)? {
|
||||
let sub_entry = sub_entry?;
|
||||
let sub_path = sub_entry.path();
|
||||
let target_path = target_dir.join(sub_entry.file_name());
|
||||
if sub_path.is_dir() {
|
||||
std::fs::create_dir_all(&target_path)?;
|
||||
// Recursively copy directory contents
|
||||
copy_dir_all(&sub_path, &target_path)?;
|
||||
} else {
|
||||
std::fs::copy(&sub_path, &target_path)?;
|
||||
copy_file_times(&sub_path, &target_path)?;
|
||||
}
|
||||
}
|
||||
std::fs::remove_dir_all(&src_dir)?;
|
||||
} else {
|
||||
std::fs::rename(&src_dir, &target_dir)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -679,7 +714,7 @@ mod tests {
|
||||
let cwd = temp_dir.path();
|
||||
|
||||
// Main 'pull' command: the one we want to test
|
||||
let info = crate::package_info::lookup(package, None, series, "", dist, None, None)
|
||||
let info = crate::package_info::lookup(package, None, series, "", dist, None, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
pull(&info, Some(cwd), None, archive.unwrap_or(false))
|
||||
|
||||
Reference in New Issue
Block a user