brum 0.8.4

Multi-Pane Web Environment (File Commander/Manager) - By Woofson
Documentation
use super::{DirectoryListing, FileContentResponse, FileEntry};
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use flate2::Compression;
use std::fs::{self, File};
use std::io::{BufReader, Read};
use std::path::Path;
use tar::Archive as TarArchive;
use tar::Builder as TarBuilder;
use tracing::info;
use zip::{ZipArchive, ZipWriter};

pub struct ArchiveHandler;

impl ArchiveHandler {
    pub fn list_archive_contents(
        archive_path_str: &str,
        subpath_filter: &str,
    ) -> Result<DirectoryListing, std::io::Error> {
        let path = Path::new(archive_path_str);
        if !path.exists() {
            return Err(std::io::Error::new(
                std::io::ErrorKind::NotFound,
                format!("Archive not found: {}", archive_path_str),
            ));
        }

        let lower_name = archive_path_str.to_lowercase();
        let mut entries = Vec::new();
        let mut total_files = 0;
        let mut total_dirs = 0;
        let mut total_size = 0u64;

        let clean_subpath = subpath_filter.trim_matches('/');

        if lower_name.ends_with(".zip") || lower_name.ends_with(".cbz") || lower_name.ends_with(".epub") {
            let file = File::open(path)?;
            let mut zip = ZipArchive::new(BufReader::new(file))
                .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;

            for i in 0..zip.len() {
                let file = zip.by_index(i)
                    .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
                let raw_name = file.name().to_string();
                let is_dir = file.is_dir() || raw_name.ends_with('/');
                let trimmed = raw_name.trim_matches('/');

                if !clean_subpath.is_empty() {
                    if !trimmed.starts_with(clean_subpath) {
                        continue;
                    }
                }

                let display_name = if clean_subpath.is_empty() {
                    trimmed.split('/').next().unwrap_or("").to_string()
                } else {
                    let suffix = trimmed.strip_prefix(clean_subpath).unwrap_or("").trim_matches('/');
                    suffix.split('/').next().unwrap_or("").to_string()
                };

                if display_name.is_empty() || entries.iter().any(|e: &FileEntry| e.name == display_name) {
                    continue;
                }

                let size = file.size();
                if is_dir {
                    total_dirs += 1;
                } else {
                    total_files += 1;
                    total_size += size;
                }

                entries.push(FileEntry {
                    name: display_name.clone(),
                    path: format!("archive://{}#{}", archive_path_str, trimmed),
                    is_dir,
                    is_symlink: false,
                    is_empty: None,
                    size,
                    modified: None,
                    permissions: if is_dir { "drwxr-xr-x".to_string() } else { "-rw-r--r--".to_string() },
                    mode_octal: if is_dir { "0755".to_string() } else { "0644".to_string() },
                    owner: "archive".to_string(),
                    group: "archive".to_string(),
                    uid: 1000,
                    gid: 1000,
                    mime_type: if is_dir { None } else { Some(mime_guess::from_path(&display_name).first_or_octet_stream().to_string()) },
                    is_archive: false,
                });
            }
        } else if lower_name.ends_with(".tar.gz") || lower_name.ends_with(".tgz") || lower_name.ends_with(".tar") {
            let file = File::open(path)?;
            let reader: Box<dyn Read> = if lower_name.ends_with(".tar.gz") || lower_name.ends_with(".tgz") {
                Box::new(GzDecoder::new(file))
            } else {
                Box::new(file)
            };

            let mut tar = TarArchive::new(reader);
            if let Ok(entries_iter) = tar.entries() {
                for entry_res in entries_iter {
                    if let Ok(entry) = entry_res {
                        if let Ok(path_buf) = entry.path() {
                            let raw_name = path_buf.to_string_lossy().to_string();
                            let is_dir = entry.header().entry_type().is_dir();
                            let trimmed = raw_name.trim_matches('/');

                            if !clean_subpath.is_empty() && !trimmed.starts_with(clean_subpath) {
                                continue;
                            }

                            let display_name = if clean_subpath.is_empty() {
                                trimmed.split('/').next().unwrap_or("").to_string()
                            } else {
                                let suffix = trimmed.strip_prefix(clean_subpath).unwrap_or("").trim_matches('/');
                                suffix.split('/').next().unwrap_or("").to_string()
                            };

                            if display_name.is_empty() || entries.iter().any(|e: &FileEntry| e.name == display_name) {
                                continue;
                            }

                            let size = entry.header().size().unwrap_or(0);
                            if is_dir {
                                total_dirs += 1;
                            } else {
                                total_files += 1;
                                total_size += size;
                            }

                            entries.push(FileEntry {
                                name: display_name.clone(),
                                path: format!("archive://{}#{}", archive_path_str, trimmed),
                                is_dir,
                                is_symlink: false,
                                is_empty: None,
                                size,
                                modified: None,
                                permissions: if is_dir { "drwxr-xr-x".to_string() } else { "-rw-r--r--".to_string() },
                                mode_octal: if is_dir { "0755".to_string() } else { "0644".to_string() },
                                owner: "archive".to_string(),
                                group: "archive".to_string(),
                                uid: 1000,
                                gid: 1000,
                                mime_type: if is_dir { None } else { Some(mime_guess::from_path(&display_name).first_or_octet_stream().to_string()) },
                                is_archive: false,
                            });
                        }
                    }
                }
            }
        }

        entries.sort_by(|a, b| {
            if a.is_dir == b.is_dir {
                a.name.to_lowercase().cmp(&b.name.to_lowercase())
            } else if a.is_dir {
                std::cmp::Ordering::Less
            } else {
                std::cmp::Ordering::Greater
            }
        });

        Ok(DirectoryListing {
            current_path: format!("archive://{}#{}", archive_path_str, clean_subpath),
            parent_path: if !clean_subpath.is_empty() {
                Some(format!("archive://{}#", archive_path_str))
            } else {
                Some(Path::new(archive_path_str).parent().unwrap_or(Path::new("/")).to_string_lossy().to_string())
            },
            entries,
            total_files,
            total_dirs,
            total_size,
            protocol: "archive".to_string(),
            is_truncated: None,
            max_limit: None,
        })
    }

    pub fn read_archive_entry(
        archive_path_str: &str,
        inner_path: &str,
        max_bytes: usize,
    ) -> Result<FileContentResponse, std::io::Error> {
        let path = Path::new(archive_path_str);
        let lower = archive_path_str.to_lowercase();
        let clean_inner = inner_path.trim_matches('/');

        if lower.ends_with(".zip") || lower.ends_with(".cbz") || lower.ends_with(".epub") {
            let file = File::open(path)?;
            let mut zip = ZipArchive::new(BufReader::new(file))
                .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;

            for i in 0..zip.len() {
                let mut f = zip.by_index(i)
                    .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
                if f.name().trim_matches('/') == clean_inner {
                    let mut buffer = Vec::new();
                    let size = f.size();
                    if max_bytes > 0 {
                        let mut handle = (&mut f).take(max_bytes as u64);
                        handle.read_to_end(&mut buffer)?;
                    } else {
                        f.read_to_end(&mut buffer)?;
                    }

                    let mime_type = mime_guess::from_path(clean_inner).first_or_octet_stream().to_string();
                    let name = Path::new(clean_inner).file_name().unwrap_or_default().to_string_lossy().to_string();

                    return match String::from_utf8(buffer.clone()) {
                        Ok(text) => Ok(FileContentResponse {
                            path: format!("archive://{}#{}", archive_path_str, clean_inner),
                            name,
                            content: text,
                            is_binary: false,
                            size,
                            mime_type,
                        }),
                        Err(_) => {
                            use base64::Engine;
                            let b64 = base64::engine::general_purpose::STANDARD.encode(&buffer);
                            Ok(FileContentResponse {
                                path: format!("archive://{}#{}", archive_path_str, clean_inner),
                                name,
                                content: b64,
                                is_binary: true,
                                size,
                                mime_type,
                            })
                        }
                    };
                }
            }
        }

        Err(std::io::Error::new(std::io::ErrorKind::NotFound, "Entry not found in archive"))
    }

    pub fn extract_archive(archive_path_str: &str, target_dir_str: &str) -> Result<(), std::io::Error> {
        let path = Path::new(archive_path_str);
        let target = Path::new(target_dir_str);
        fs::create_dir_all(target)?;

        let lower = archive_path_str.to_lowercase();

        if lower.ends_with(".zip") || lower.ends_with(".cbz") || lower.ends_with(".epub") {
            let file = File::open(path)?;
            let mut zip = ZipArchive::new(BufReader::new(file))
                .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
            zip.extract(target)
                .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
            info!("Extracted zip {} to {}", archive_path_str, target_dir_str);
            Ok(())
        } else if lower.ends_with(".tar.gz") || lower.ends_with(".tgz") {
            let file = File::open(path)?;
            let tar = GzDecoder::new(file);
            let mut archive = TarArchive::new(tar);
            archive.unpack(target)?;
            info!("Extracted tar.gz {} to {}", archive_path_str, target_dir_str);
            Ok(())
        } else if lower.ends_with(".tar") {
            let file = File::open(path)?;
            let mut archive = TarArchive::new(file);
            archive.unpack(target)?;
            info!("Extracted tar {} to {}", archive_path_str, target_dir_str);
            Ok(())
        } else if lower.ends_with(".tar.bz2") || lower.ends_with(".tbz2") {
            let file = File::open(path)?;
            let bz = bzip2::read::BzDecoder::new(file);
            let mut archive = TarArchive::new(bz);
            archive.unpack(target)?;
            info!("Extracted tar.bz2 {} to {}", archive_path_str, target_dir_str);
            Ok(())
        } else {
            let status = std::process::Command::new("tar")
                .arg("-xf")
                .arg(archive_path_str)
                .arg("-C")
                .arg(target_dir_str)
                .status();

            if let Ok(s) = status {
                if s.success() {
                    return Ok(());
                }
            }

            let status_7z = std::process::Command::new("7z")
                .arg("x")
                .arg(format!("-o{}", target_dir_str))
                .arg(archive_path_str)
                .arg("-y")
                .status();

            if let Ok(s) = status_7z {
                if s.success() {
                    return Ok(());
                }
            }

            Err(std::io::Error::new(
                std::io::ErrorKind::Other,
                format!("Unsupported archive format for extraction: {}", archive_path_str),
            ))
        }
    }

    pub fn create_zip(source_paths: &[String], target_archive: &str) -> Result<(), std::io::Error> {
        let file = File::create(target_archive)?;
        let mut zip = ZipWriter::new(file);
        let options = zip::write::SimpleFileOptions::default()
            .compression_method(zip::CompressionMethod::Deflated)
            .unix_permissions(0o755);

        for src_str in source_paths {
            let src_path = Path::new(src_str);
            let base_name = src_path.file_name().unwrap_or_default().to_string_lossy();

            if src_path.is_dir() {
                for entry in walkdir::WalkDir::new(src_path) {
                    let entry = entry.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
                    let rel = entry.path().strip_prefix(src_path).unwrap();
                    let entry_name = if rel.as_os_str().is_empty() {
                        format!("{}/", base_name)
                    } else {
                        format!("{}/{}", base_name, rel.to_string_lossy().replace('\\', "/"))
                    };

                    if entry.file_type().is_dir() {
                        zip.add_directory(&entry_name, options)
                            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
                    } else {
                        zip.start_file(&entry_name, options)
                            .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
                        let mut f = File::open(entry.path())?;
                        std::io::copy(&mut f, &mut zip)?;
                    }
                }
            } else if src_path.is_file() {
                zip.start_file(base_name.as_ref(), options)
                    .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
                let mut f = File::open(src_path)?;
                std::io::copy(&mut f, &mut zip)?;
            }
        }

        zip.finish().map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
        info!("Created zip archive: {}", target_archive);
        Ok(())
    }

    pub fn create_targz(source_paths: &[String], target_archive: &str) -> Result<(), std::io::Error> {
        let tar_gz = File::create(target_archive)?;
        let enc = GzEncoder::new(tar_gz, Compression::default());
        let mut tar = TarBuilder::new(enc);

        for src_str in source_paths {
            let src_path = Path::new(src_str);
            let base_name = src_path.file_name().unwrap_or_default().to_string_lossy();

            if src_path.is_dir() {
                tar.append_dir_all(base_name.as_ref(), src_path)?;
            } else if src_path.is_file() {
                let mut file = File::open(src_path)?;
                tar.append_file(base_name.as_ref(), &mut file)?;
            }
        }

        tar.finish()?;
        info!("Created tar.gz archive: {}", target_archive);
        Ok(())
    }
}