/* This file is part of jellything (https://codeberg.org/metamuffin/jellything) which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2023 metamuffin */ use anyhow::{anyhow, bail, Context, Ok}; use jellycommon::{DirectoryInfo, ItemInfo}; use log::info; use std::{ ffi::OsStr, fs::File, os::unix::prelude::OsStrExt, path::{Path, PathBuf}, sync::Arc, }; pub struct Library { pub root: Arc, pub root_path: PathBuf, } #[derive(Debug, Clone)] pub enum Node { Directory(Arc), Item(Arc), } #[derive(Debug, Clone)] pub struct Directory { pub lib_path: PathBuf, pub identifier: String, pub info: DirectoryInfo, pub children: Vec>, } #[derive(Debug, Clone)] pub struct Item { pub fs_path: PathBuf, pub lib_path: PathBuf, pub identifier: String, pub info: ItemInfo, } impl Library { pub fn open(path: &Path) -> anyhow::Result { Ok(Self { root_path: path.to_path_buf(), root: Node::from_path(path.to_path_buf(), PathBuf::new(), true) .context("indexing root")? .into_iter() .next() .ok_or(anyhow!("root need directory.json"))?, }) } pub fn nested_path(&self, path: &Path) -> anyhow::Result> { self.nested(path.to_str().unwrap()) } pub fn nested(&self, path: &str) -> anyhow::Result> { let mut n = self.root.clone(); if path.is_empty() { return Ok(n); } for seg in path.split('/') { n = n .get_directory()? .child_by_ident(seg) .ok_or(anyhow!("does not exist"))?; } Ok(n) } } impl Node { pub fn get_directory(&self) -> anyhow::Result> { match self { Node::Directory(d) => Ok(d.clone()), Node::Item(_) => bail!("not a directory"), } } pub fn get_item(&self) -> anyhow::Result> { match self { Node::Item(i) => Ok(i.clone()), Node::Directory(_) => bail!("not an item"), } } pub fn title(&self) -> &str { match self { Node::Directory(d) => &d.info.name, Node::Item(i) => &i.info.title, } } pub fn identifier(&self) -> &str { match self { Node::Directory(d) => &d.identifier, Node::Item(i) => &i.identifier, } } pub fn banner(&self) -> &Option { match self { Node::Directory(d) => &d.info.banner, Node::Item(i) => &i.info.banner, } } pub fn from_path( path: PathBuf, mut lib_path: PathBuf, root: bool, ) -> anyhow::Result>> { if path.is_dir() { let mpath = path.join("directory.json"); let children = path.read_dir()?.filter_map(|e| { let e = e.unwrap(); if e.path().extension() == Some(&OsStr::from_bytes(b"jelly")) || e.metadata().unwrap().is_dir() { Some(e.path()) } else { None } }); if !mpath.exists() { info!("flattening {path:?}"); Ok(children .map(|e| Node::from_path(e, lib_path.clone(), false)) .collect::, _>>()? .into_iter() .flatten() .collect()) } else { let data: DirectoryInfo = serde_json::from_reader(File::open(mpath).context("metadata missing")?)?; let identifier = path.file_name().unwrap().to_str().unwrap().to_string(); if !root { lib_path = lib_path.join(identifier.clone()); } info!("scanning directory {path:?}"); let children = children .map(|e| { Node::from_path(e.clone(), lib_path.clone(), false) .context(format!("loading {e:?}")) }) .collect::>>()? .into_iter() .flatten() .collect(); Ok(Vec::from_iter(Some( Node::Directory(Arc::new(Directory { lib_path, children, info: data, identifier, })) .into(), ))) } } else if path.is_file() { info!("loading item {path:?}"); let datafile = File::open(path.clone()).context("cant load metadata")?; let data: ItemInfo = serde_json::from_reader(datafile).context("invalid metadata")?; let identifier = path .file_name() .unwrap() .to_str() .unwrap() .strip_suffix(".jelly") .unwrap() .to_string(); Ok(Vec::from_iter(Some( Node::Item(Arc::new(Item { fs_path: path, lib_path: lib_path.join(identifier.clone()), info: data, identifier, })) .into(), ))) } else { bail!("did somebody really put a fifo or socket in the library?!") } } } impl Item { pub fn path(&self) -> String { self.lib_path.to_str().unwrap().to_string() } } impl Directory { pub fn path(&self) -> String { self.lib_path.to_str().unwrap().to_string() } pub fn child_by_ident(&self, i: &str) -> Option> { self.children .iter() .find(|e| e.identifier() == i) .map(|e| e.to_owned()) } }