use age::{armor::ArmoredReader, stream::StreamReader};
use fuse_mt::*;
use std::collections::HashMap;
use std::fs::File;
use std::io::{self, BufReader, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::sync::{mpsc, Mutex};
use std::time::{Duration, SystemTime};
use tar::{Archive, Entry, EntryType};
fn tar_path(path: &Path) -> &Path {
path.strip_prefix("/").unwrap()
}
fn tar_to_filetype<R: Read>(entry: &Entry<R>) -> Option<FileType> {
match entry.header().entry_type() {
EntryType::Regular => Some(FileType::RegularFile),
EntryType::Directory => Some(FileType::Directory),
EntryType::Continuous => Some(FileType::RegularFile),
EntryType::GNULongName => Some(FileType::RegularFile),
_ => None,
}
}
fn tar_to_fuse<R: Read>(entry: &Entry<R>) -> io::Result<FileAttr> {
let kind = tar_to_filetype(entry)
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Unsupported filetype"))?;
let perm = (entry.header().mode()? & 0o7777) as u16;
let mtime = SystemTime::UNIX_EPOCH + Duration::new(entry.header().mtime()? as u64, 0);
let ctime = if let Some(header) = entry.header().as_gnu() {
header
.ctime()
.map(|ctime| SystemTime::UNIX_EPOCH + Duration::new(ctime as u64, 0))
.unwrap_or(mtime)
} else {
mtime
};
let atime = if let Some(header) = entry.header().as_gnu() {
header
.atime()
.map(|atime| SystemTime::UNIX_EPOCH + Duration::new(atime as u64, 0))
.unwrap_or(mtime)
} else {
mtime
};
Ok(FileAttr {
size: entry.header().size()?,
blocks: 1,
atime,
mtime,
ctime,
crtime: SystemTime::UNIX_EPOCH,
kind,
perm,
nlink: 1,
uid: entry.header().uid()? as u32,
gid: entry.header().gid()? as u32,
rdev: 0,
flags: 0,
})
}
const ROOT_ATTR: FileAttr = FileAttr {
size: 0,
blocks: 0,
atime: SystemTime::UNIX_EPOCH,
mtime: SystemTime::UNIX_EPOCH,
ctime: SystemTime::UNIX_EPOCH,
crtime: SystemTime::UNIX_EPOCH,
kind: FileType::Directory,
perm: 0o0755,
nlink: 1,
uid: 1000,
gid: 1000,
rdev: 0,
flags: 0,
};
fn add_to_dir_map(
dir_map: &mut HashMap<PathBuf, Vec<DirectoryEntry>>,
path: &Path,
kind: FileType,
) {
let name = path
.file_name()
.expect("All files in tarballs have names")
.to_owned();
let parent = path
.parent()
.expect("paths should have parents")
.to_path_buf();
dir_map
.entry(parent)
.or_default()
.push(DirectoryEntry { name, kind });
}
type OpenFile = (PathBuf, u64, u64);
pub struct AgeTarFs {
inner: Mutex<StreamReader<ArmoredReader<BufReader<File>>>>,
destroy_tx: mpsc::SyncSender<()>,
dir_map: HashMap<PathBuf, Vec<DirectoryEntry>>,
file_map: HashMap<PathBuf, (FileAttr, u64)>,
open_dirs: Mutex<(HashMap<u64, PathBuf>, u64)>,
open_files: Mutex<(HashMap<u64, OpenFile>, u64)>,
}
impl AgeTarFs {
pub fn open(
stream: StreamReader<ArmoredReader<BufReader<File>>>,
destroy_tx: mpsc::SyncSender<()>,
) -> io::Result<Self> {
let mut dir_map: HashMap<PathBuf, Vec<DirectoryEntry>> = HashMap::new();
dir_map.insert(PathBuf::new(), vec![]);
let mut file_map: HashMap<PathBuf, (FileAttr, u64)> = HashMap::new();
let mut archive = Archive::new(stream);
for file in archive.entries().expect("StreamReader is at start") {
let file = file?;
if let Some(filetype) = tar_to_filetype(&file) {
let path = file.path()?;
add_to_dir_map(&mut dir_map, &path, filetype);
file_map.insert(
path.to_path_buf(),
(tar_to_fuse(&file)?, file.raw_file_position()),
);
}
}
Ok(AgeTarFs {
inner: Mutex::new(archive.into_inner()),
destroy_tx,
dir_map,
file_map,
open_dirs: Mutex::new((HashMap::new(), 0)),
open_files: Mutex::new((HashMap::new(), 0)),
})
}
}
const TTL: Duration = Duration::from_secs(1);
impl FilesystemMT for AgeTarFs {
fn destroy(&self) {
self.destroy_tx
.send(())
.expect("Could not send signal on channel.");
}
fn getattr(&self, _req: RequestInfo, path: &Path, fh: Option<u64>) -> ResultEntry {
let open_dirs = self.open_dirs.lock().unwrap();
let open_files = self.open_files.lock().unwrap();
if let Some(fh) = fh {
if let Some((attr, _)) = open_dirs
.0
.get(&fh)
.and_then(|path| self.file_map.get(path))
{
Ok((TTL, *attr))
} else if let Some((attr, _)) = open_files
.0
.get(&fh)
.and_then(|(path, _, _)| self.file_map.get(path))
{
Ok((TTL, *attr))
} else {
Err(libc::EBADF)
}
} else {
let path = tar_path(path);
if path.parent().is_none() {
Ok((TTL, ROOT_ATTR))
} else if let Some((attr, _)) = self.file_map.get(path) {
Ok((TTL, *attr))
} else {
Err(libc::ENOENT)
}
}
}
fn opendir(&self, _req: RequestInfo, path: &Path, _flags: u32) -> ResultOpen {
let mut open_dirs = self.open_dirs.lock().unwrap();
let fh = open_dirs.1;
let path = tar_path(path);
open_dirs.0.insert(fh, path.to_path_buf());
open_dirs.1 = open_dirs.1.wrapping_add(1);
Ok((fh, 0))
}
fn readdir(&self, _req: RequestInfo, _path: &Path, fh: u64) -> ResultReaddir {
let open_dirs = self.open_dirs.lock().unwrap();
if let Some(path) = open_dirs.0.get(&fh) {
Ok(self.dir_map.get(path).cloned().unwrap_or_default())
} else {
Err(libc::EBADF)
}
}
fn releasedir(&self, _req: RequestInfo, _path: &Path, fh: u64, _flags: u32) -> ResultEmpty {
let mut open_dirs = self.open_dirs.lock().unwrap();
open_dirs.0.remove(&fh).map(|_| ()).ok_or(libc::EBADF)
}
fn statfs(&self, _req: RequestInfo, _path: &Path) -> ResultStatfs {
Ok(Statfs {
blocks: self.file_map.len() as u64,
bfree: 0,
bavail: 0,
files: self.file_map.len() as u64,
ffree: 0,
bsize: 64 * 1024,
namelen: u32::max_value(),
frsize: 64 * 1024,
})
}
fn open(&self, _req: RequestInfo, path: &Path, _flags: u32) -> ResultOpen {
let mut open_files = self.open_files.lock().unwrap();
if let Some((attr, pos)) = self.file_map.get(tar_path(path)) {
let fh = open_files.1;
open_files
.0
.insert(fh, (path.to_path_buf(), *pos, attr.size));
open_files.1 = open_files.1.wrapping_add(1);
Ok((fh, 0))
} else {
Err(libc::ENOENT)
}
}
fn read(
&self,
_req: RequestInfo,
_path: &Path,
fh: u64,
offset: u64,
size: u32,
callback: impl FnOnce(ResultSlice<'_>) -> CallbackResult,
) -> CallbackResult {
let mut inner = self.inner.lock().unwrap();
let open_files = self.open_files.lock().unwrap();
if let Some((_, pos, file_size)) = open_files.0.get(&fh) {
if offset > *file_size {
return callback(Err(libc::EINVAL));
}
if inner.seek(SeekFrom::Start(pos + offset)).is_err() {
return callback(Err(libc::EIO));
}
let to_read = usize::min(size as usize, (file_size - offset) as usize);
let mut buf = vec![];
buf.resize(to_read, 0);
match inner.read_exact(&mut buf) {
Ok(_) => callback(Ok(&buf)),
Err(_) => callback(Err(libc::EIO)),
}
} else {
callback(Err(libc::EBADF))
}
}
fn release(
&self,
_req: RequestInfo,
_path: &Path,
fh: u64,
_flags: u32,
_lock_owner: u64,
_flush: bool,
) -> ResultEmpty {
let mut open_files = self.open_files.lock().unwrap();
open_files.0.remove(&fh).map(|_| ()).ok_or(libc::EBADF)
}
}