Tar
Tar archives, gzipped or not: made from files and folders, listed, one file read, and opened safely into a folder.
Version 1.0.0 · 4 functions · 2 crates
Functions
What use tar.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use tar_create, tar_list from tar (Quillo 0.6).
tar_create(to: str, files: list, base: str) → intor an errorMakes the archive to from files, paths inside the folder base (folders are taken whole); gzipped when to ends in .gz or .tgz. Names in the archive are relative to base. The number of files put in.
tar_list(path: str) → listor an errorThe archive's entries as { name, size, kind, modified }: kind is file, dir, symlink or other; size in bytes; modified in seconds since 1970.
tar_read_file(path: str, name: str) → stror an errorThe text of one file in the archive, by its name there (as tar_list gives it); an error when it is missing. Up to 64 MB.
tar_extract(path: str, to: str) → listor an errorOpens the archive into the folder to (made if needed): the paths written. Safe for archives from anywhere: names that would land outside to (absolute, or with ".."), links and devices are skipped, and it stops past 100,000 entries or 4 GB.
Get it
$ quillo install tar # tar.qwer into this folder$ quillo install tar --example # and its example, tar.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/tar.qwer$ curl -O https://quillolang.org/plugins/files/tar.qweThen, in your program, all of it or only what you call:
use tar.qweruse tar_create, tar_list from tar // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
tar = { version = "0.4", default-features = false }flate2 = "1"Example
// A nightly backup of a folder as a .tar.gz, listed, read and restored.//// quillo run tar.qwe then open http://localhost:8080/backupuse "tar.qwer"route GET /backup { write_file("data/site/orders.csv", "id,total\n1,120.50\n2,99.00\n") write_file("data/site/pages/about.md", "# About us\n\nWe roast on Mondays.\n") let count = tar_create("data/backup.tar.gz", ["orders.csv", "pages"], "data/site")? { files: count, list: tar_list("data/backup.tar.gz")? | filter((e) => e.kind == "file") | map((e) => e.name), about: tar_read_file("data/backup.tar.gz", "pages/about.md")?, restored: tar_extract("data/backup.tar.gz", "data/restored")?, }}route GET /backup.tar.gz = file("data/backup.tar.gz")$ quillo run tar.qwe$ curl 'localhost:8080/backup'Source
//! Tar archives, gzipped or not: made from files and folders, listed, one file read, and opened safely into a folder.//! version: 1.0.0//!//! [dependencies]//! tar = { version = "0.4", default-features = false }//! flate2 = "1"use flate2::read::GzDecoder;use flate2::write::GzEncoder;use flate2::Compression;use serde_json::{json, Value};use std::io::Read;use std::path::{Component, Path, PathBuf};const MAX_ENTRIES: usize = 100_000;const MAX_TOTAL: u64 = 4 * 1024 * 1024 * 1024;const MAX_TEXT: u64 = 64 * 1024 * 1024;fn gz(path: &str) -> bool { let p = path.to_lowercase(); p.ends_with(".gz") || p.ends_with(".tgz")}fn reader(path: &str) -> Result<Box<dyn Read>, String> { let mut f = std::fs::File::open(path).map_err(|e| format!("{}: {}", path, e))?; let mut magic = [0u8; 2]; let n = f.read(&mut magic).map_err(|e| e.to_string())?; let f = std::fs::File::open(path).map_err(|e| format!("{}: {}", path, e))?; // Gzipped or not is read from the first bytes, whatever the name says. Ok(if n == 2 && magic == [0x1f, 0x8b] { Box::new(GzDecoder::new(f)) } else { Box::new(f) })}// A name inside the archive that stays inside the folder: no absolute paths, no "..".fn safe(name: &Path) -> Option<PathBuf> { let mut out = PathBuf::new(); for c in name.components() { match c { Component::Normal(p) => out.push(p), Component::CurDir => {} _ => return None, } } if out.as_os_str().is_empty() { None } else { Some(out) }}/// Makes the archive `to` from `files`, paths inside the folder `base` (folders are taken whole); gzipped when `to` ends in .gz or .tgz./// Names in the archive are relative to `base`. The number of files put in.pub fn tar_create(to: &str, files: Vec<String>, base: &str) -> Result<i64, String> { let base = Path::new(if base.is_empty() { "." } else { base }); if let Some(dir) = Path::new(to).parent().filter(|d| !d.as_os_str().is_empty()) { std::fs::create_dir_all(dir).map_err(|e| format!("{}: {}", dir.display(), e))?; } let out = std::fs::File::create(to).map_err(|e| format!("{}: {}", to, e))?; let w: Box<dyn std::io::Write> = if gz(to) { Box::new(GzEncoder::new(out, Compression::new(6))) } else { Box::new(out) }; let mut b = tar::Builder::new(w); b.follow_symlinks(false); let mut count = 0i64; for f in &files { let name = safe(Path::new(f)).ok_or_else(|| format!("`{}` is not a path inside the folder", f))?; let full = base.join(&name); let meta = std::fs::metadata(&full).map_err(|e| format!("{}: {}", full.display(), e))?; if meta.is_dir() { b.append_dir_all(&name, &full).map_err(|e| format!("{}: {}", full.display(), e))?; count += walk_count(&full); } else { b.append_path_with_name(&full, &name).map_err(|e| format!("{}: {}", full.display(), e))?; count += 1; } } let w = b.into_inner().map_err(|e| e.to_string())?; drop(w); Ok(count)}fn walk_count(dir: &Path) -> i64 { std::fs::read_dir(dir).map(|rd| rd.flatten().map(|e| if e.path().is_dir() { walk_count(&e.path()) } else { 1 }).sum()).unwrap_or(0)}/// The archive's entries as `{ name, size, kind, modified }`: `kind` is file, dir, symlink or other; `size` in bytes; `modified` in seconds since 1970.pub fn tar_list(path: &str) -> Result<Vec<Value>, String> { let mut a = tar::Archive::new(reader(path)?); let mut out = vec![]; for e in a.entries().map_err(|e| format!("{}: {}", path, e))? { let e = e.map_err(|e| format!("{}: {}", path, e))?; let h = e.header(); let kind = match h.entry_type() { tar::EntryType::Regular | tar::EntryType::Continuous => "file", tar::EntryType::Directory => "dir", tar::EntryType::Symlink | tar::EntryType::Link => "symlink", _ => "other", }; out.push(json!({ "name": e.path().map(|p| p.to_string_lossy().into_owned()).unwrap_or_default(), "size": h.size().unwrap_or(0), "kind": kind, "modified": h.mtime().unwrap_or(0) })); if out.len() >= MAX_ENTRIES { break; } } Ok(out)}/// The text of one file in the archive, by its name there (as `tar_list` gives it); an error when it is missing. Up to 64 MB.pub fn tar_read_file(path: &str, name: &str) -> Result<String, String> { let want = name.trim_start_matches("./"); let mut a = tar::Archive::new(reader(path)?); for e in a.entries().map_err(|e| format!("{}: {}", path, e))? { let e = e.map_err(|e| format!("{}: {}", path, e))?; let n = e.path().map(|p| p.to_string_lossy().into_owned()).unwrap_or_default(); if n.trim_start_matches("./") == want && e.header().entry_type().is_file() { let mut s = String::new(); e.take(MAX_TEXT).read_to_string(&mut s).map_err(|e| format!("{}: {}", name, e))?; return Ok(s); } } Err(format!("{}: no file `{}` in it", path, name))}/// Opens the archive into the folder `to` (made if needed): the paths written. Safe for archives from anywhere:/// names that would land outside `to` (absolute, or with ".."), links and devices are skipped, and it stops past 100,000 entries or 4 GB.pub fn tar_extract(path: &str, to: &str) -> Result<Vec<String>, String> { std::fs::create_dir_all(to).map_err(|e| format!("{}: {}", to, e))?; let mut a = tar::Archive::new(reader(path)?); a.set_preserve_permissions(false); a.set_unpack_xattrs(false); let (mut written, mut total, mut seen) = (vec![], 0u64, 0usize); for e in a.entries().map_err(|e| format!("{}: {}", path, e))? { let mut e = e.map_err(|e| format!("{}: {}", path, e))?; seen += 1; if seen > MAX_ENTRIES { return Err(format!("{}: more than {} entries", path, MAX_ENTRIES)); } let ty = e.header().entry_type(); if !(ty.is_file() || ty.is_dir()) { continue; } let Some(rel) = e.path().ok().and_then(|p| safe(&p)) else { continue }; total += e.header().size().unwrap_or(0); if total > MAX_TOTAL { return Err(format!("{}: more than 4 GB unpacked", path)); } let dest = Path::new(to).join(&rel); if ty.is_dir() { std::fs::create_dir_all(&dest).map_err(|e| format!("{}: {}", dest.display(), e))?; continue; } if let Some(parent) = dest.parent() { std::fs::create_dir_all(parent).map_err(|e| format!("{}: {}", parent.display(), e))?; } let mut out = std::fs::File::create(&dest).map_err(|e| format!("{}: {}", dest.display(), e))?; std::io::copy(&mut e, &mut out).map_err(|e| format!("{}: {}", dest.display(), e))?; written.push(dest.to_string_lossy().into_owned()); } Ok(written)}Related
sevenz.qwer · 1.0.07-Zip archives listed, one file read, opened safely into a folder, and made from a folder.7 views · 8 downloadsbzip2 bzip2.qwer · 1.0.0bzip2 compression of files and text in pure Rust, .bz2 dumps unpacked, and their first lines read without unpacking them all.6 views · 7 downloadsxz xz.qwer · 1.0.0xz (LZMA2) compression of files and text, and old .lzma files read: the smallest archives, for backups and downloads.6 views · 8 downloadsZstandard zstd.qwer · 1.0.0Zstandard compression of files and text: faster than gzip at the same size, for backups, logs and caches.8 views · 6 downloadsGzip gzip.qwer · 1.0.0Gzip files and text, and unpack them: smaller backups, exports and payloads.7 views · 6 downloadsBrotli brotli.qwer · 1.0.0Brotli compression of files and text: smaller than gzip for HTML, CSS and JavaScript served pre-compressed.7 views · 9 downloadsThread 0 posts
On Tar: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.