aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/baas.rs224
-rw-r--r--src/contentdb.rs127
-rw-r--r--src/download.rs161
-rw-r--r--src/error.rs64
-rw-r--r--src/game.rs74
-rw-r--r--src/kvstore.rs49
-rw-r--r--src/lib.rs104
-rw-r--r--src/minemod.rs242
-rw-r--r--src/util.rs62
-rw-r--r--src/world.rs97
10 files changed, 0 insertions, 1204 deletions
diff --git a/src/baas.rs b/src/baas.rs
deleted file mode 100644
index 938b4c4..0000000
--- a/src/baas.rs
+++ /dev/null
@@ -1,224 +0,0 @@
-//! This module contains functions to query & manipulate the global Minetest installation.
-use std::{collections::HashMap, path::PathBuf};
-
-use dirs;
-use log::debug;
-
-use super::{
- error::Result,
- game::Game,
- minemod::{self, MineMod},
- scan,
- world::World,
-};
-
-/// Returns a list of folders in which worlds are expected.
-///
-/// Note that not all of these folders need to exist.
-///
-/// This returns the following locations:
-///
-/// * `$HOME/.minetest/worlds`
-/// * `/var/games/minetest-server/.minetest/worlds`
-pub fn world_dirs() -> Result<Vec<PathBuf>> {
- let mut paths = vec!["/var/games/minetest-server/.minetest/worlds".into()];
- if let Some(home) = dirs::home_dir() {
- paths.push(home.join(".minetest").join("worlds"))
- }
- Ok(paths)
-}
-
-/// Returns a list of folders in which games are expected.
-///
-/// Note that not all of these folders need to exist.
-///
-/// This returns the following locations:
-///
-/// * `$HOME/.minetest/games`
-/// * `/var/games/minetest-server/.minetest/games`
-/// * `/usr/share/minetest/games`
-/// * `/usr/share/games/minetest/games`
-pub fn game_dirs() -> Result<Vec<PathBuf>> {
- let mut paths = vec![
- "/var/games/minetest-server/.minetest/games".into(),
- "/usr/share/minetest/games".into(),
- "/usr/share/games/minetest/games".into(),
- ];
- if let Some(home) = dirs::home_dir() {
- paths.push(home.join(".minetest").join("games"))
- }
- Ok(paths)
-}
-
-/// Returns a list of folders in which mods are expected.
-///
-/// Note that not all of these folders need to exist.
-///
-/// This returns the following locations:
-///
-/// * `$HOME/.minetest/mods`
-/// * `/var/games/minetest-server/.minetest/mods`
-/// * `/usr/share/games/minetest/mods`
-/// * `/usr/share/minetest/mods`
-pub fn mod_dirs() -> Result<Vec<PathBuf>> {
- let mut paths = vec![
- "/var/games/minetest-server/.minetest/mods".into(),
- "/usr/share/games/minetest/mods".into(),
- "/usr/share/minetest/mods".into(),
- ];
- if let Some(home) = dirs::home_dir() {
- paths.push(home.join(".minetest").join("mods"))
- }
- Ok(paths)
-}
-
-/// The [`Baas`] provides a way to list all worlds, games and mods on the system and allows access
-/// via the [`World`], [`Game`] and [`MineMod`] wrappers.
-#[derive(Debug, Default, Clone)]
-pub struct Baas {
- world_dirs: Vec<PathBuf>,
- game_dirs: Vec<PathBuf>,
- mod_dirs: Vec<PathBuf>,
-}
-
-impl Baas {
- /// Create a [`Baas`] with the standard dirs.
- pub fn with_standard_dirs() -> Result<Baas> {
- Ok(Baas::default()
- .with_world_dirs(world_dirs()?)
- .with_game_dirs(game_dirs()?)
- .with_mod_dirs(mod_dirs()?))
- }
-
- /// Replace the world dirs with the given list of world dirs.
- pub fn with_world_dirs(self, world_dirs: Vec<PathBuf>) -> Baas {
- Baas { world_dirs, ..self }
- }
-
- /// The list of directories which are searched for worlds.
- #[inline]
- pub fn world_dirs(&self) -> &[PathBuf] {
- self.world_dirs.as_slice()
- }
-
- /// Replace the game dirs with the given list of game dirs.
- pub fn with_game_dirs(self, game_dirs: Vec<PathBuf>) -> Baas {
- Baas { game_dirs, ..self }
- }
-
- /// The list of directories which are searched for games.
- #[inline]
- pub fn game_dirs(&self) -> &[PathBuf] {
- self.game_dirs.as_slice()
- }
-
- /// Replace the mod dirs with the given list of mod dirs.
- pub fn with_mod_dirs(self, mod_dirs: Vec<PathBuf>) -> Baas {
- Baas { mod_dirs, ..self }
- }
-
- /// The list of directories which are searched for mods.
- #[inline]
- pub fn mod_dirs(&self) -> &[PathBuf] {
- self.mod_dirs.as_slice()
- }
-
- /// Returns a vector of all words that were found in the world dirs.
- pub fn worlds(&self) -> Result<Vec<World>> {
- let mut worlds = vec![];
- for dir in self.world_dirs() {
- match scan(&dir, |p| World::open(p)) {
- Ok(w) => worlds.extend(w),
- Err(e) => debug!("Cannot scan {:?}: {}", dir, e),
- }
- }
- Ok(worlds)
- }
-
- /// Returns a vector of all games that were found in the game dirs.
- pub fn games(&self) -> Result<Vec<Game>> {
- let mut games = vec![];
- for dir in self.game_dirs() {
- match scan(&dir, |p| Game::open(p)) {
- Ok(g) => games.extend(g),
- Err(e) => debug!("Cannot scan {:?}: {}", dir, e),
- }
- }
- Ok(games)
- }
-
- /// Returns a vector of all mods that were found in the mod dirs.
- ///
- /// Note that modpacks are flattened into mods.
- pub fn mods(&self) -> Result<Vec<MineMod>> {
- let mut mods = vec![];
- for dir in self.mod_dirs() {
- match scan(&dir, |p| minemod::open_mod_or_pack(p)) {
- Ok(m) => {
- for container in m {
- mods.extend(container.mods()?);
- }
- }
- Err(e) => debug!("Cannot scan {:?}: {}", dir, e),
- }
- }
- Ok(mods)
- }
-
- /// Return a snapshot of the current state.
- ///
- /// A snapshot "freezes" the lists of worlds, mods and games in time. It is useful to avoid
- /// unnecessary I/O when it is known that the state should not have changed. It also allows
- /// fast searching of items by their name.
- pub fn snapshot(&self) -> Result<Snapshot> {
- let worlds = self.worlds()?;
- let games = self.games()?;
- let mods = self.mods()?;
-
- Ok(Snapshot {
- worlds: worlds
- .into_iter()
- .map(|w| Ok((w.world_name()?, w)))
- .collect::<Result<_>>()?,
- games: games.into_iter().map(|g| (g.technical_name(), g)).collect(),
- mods: mods
- .into_iter()
- .map(|m| Ok((m.mod_id()?, m)))
- .collect::<Result<_>>()?,
- })
- }
-}
-
-/// Snapshot of a [`Baas`] scan.
-///
-/// A snapshot is created through the [`Baas::snapshot`] method and gives a frozen view on the
-/// installed objects.
-#[derive(Debug, Clone)]
-pub struct Snapshot {
- worlds: HashMap<String, World>,
- games: HashMap<String, Game>,
- mods: HashMap<String, MineMod>,
-}
-
-impl Snapshot {
- /// Return all worlds that were found.
- ///
- /// The map maps the world's name to the [`World`] object.
- pub fn worlds(&self) -> &HashMap<String, World> {
- &self.worlds
- }
-
- /// Return all games that were found.
- ///
- /// The map maps the game ID to the [`Game`] object.
- pub fn games(&self) -> &HashMap<String, Game> {
- &self.games
- }
-
- /// Return the available mods that were found.
- ///
- /// The map maps the mod name to the [`MineMod`] object.
- pub fn mods(&self) -> &HashMap<String, MineMod> {
- &self.mods
- }
-}
diff --git a/src/contentdb.rs b/src/contentdb.rs
deleted file mode 100644
index d9c4688..0000000
--- a/src/contentdb.rs
+++ /dev/null
@@ -1,127 +0,0 @@
-//! Module to interact with the Minetest Content DB website.
-
-use once_cell::sync::Lazy;
-use scraper::{Html, Selector};
-use serde::{Deserialize, Serialize};
-use url::Url;
-
-use super::error::{Error, Result};
-
-/// The identification of content on Content DB. Consists of the username and the package name.
-pub type ContentId = (String, String);
-
-/// The URL of the default Content DB website to use.
-pub static DEFAULT_INSTANCE: Lazy<Url> =
- Lazy::new(|| Url::parse("https://content.minetest.net/").expect("Invalid default URL"));
-
-/// The metapackage selector to scrape the packages.
-static PROVIDES_SELECTOR: Lazy<Selector> =
- Lazy::new(|| Selector::parse("ul.d-flex").expect("Invalid selector"));
-
-static A_SELECTOR: Lazy<Selector> = Lazy::new(|| Selector::parse("a").expect("Invalid selector"));
-
-/// (Partial) metadata of a content item, as returned by the Content DB API.
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
-pub struct ContentMeta {
- /// Username of the author.
- pub author: String,
- /// Name of the package.
- pub name: String,
- /// A list of mods that are provided by this package.
- pub provides: Vec<String>,
- /// The short description of the package.
- pub short_description: String,
- /// The (human-readable) title of this package.
- pub title: String,
- /// The type of the package ("mod", "game", "txp")
- #[serde(rename = "type")]
- pub typ: String,
- /// The download URL of the package.
- pub url: Url,
-}
-
-/// The main access point for Content DB queries.
-#[derive(Debug, Clone)]
-pub struct ContentDb {
- base_url: Url,
-}
-
-impl Default for ContentDb {
- fn default() -> Self {
- Self::new()
- }
-}
-
-impl ContentDb {
- /// Create a new Content DB accessor pointing to the default instance.
- pub fn new() -> ContentDb {
- ContentDb {
- base_url: DEFAULT_INSTANCE.clone(),
- }
- }
-
- /// Find suitable candidates that provide the given modname.
- pub fn resolve(&self, modname: &str) -> Result<Vec<ContentMeta>> {
- let path = format!("metapackages/{}", modname);
- let endpoint = self
- .base_url
- .join(&path)
- .map_err(|_| Error::InvalidModId(modname.into()))?;
-
- let body = ureq::request_url("GET", &endpoint).call()?.into_string()?;
-
- let dom = Html::parse_document(&body);
- let provides = dom
- .select(&PROVIDES_SELECTOR)
- .next()
- .ok_or(Error::InvalidScrape)?;
-
- let candidates: Vec<ContentId> = provides
- .select(&A_SELECTOR)
- .filter_map(|a| a.value().attr("href"))
- .filter_map(extract_content_id)
- .collect();
-
- let mut good_ones = Vec::new();
-
- for (user, package) in candidates {
- let path = format!("api/packages/{}/{}/", user, package);
- let endpoint = self
- .base_url
- .join(&path)
- .expect("The parsed path was wrong");
- let response: ContentMeta = ureq::request_url("GET", &endpoint).call()?.into_json()?;
-
- // While resolving, we only care about actual mods that we can install. If a game
- // provides a certain metapackage, it is pretty much useless for us (and often just
- // there because a mod in that game provides the metapackage).
- if response.typ == "mod" {
- good_ones.push(response)
- }
- }
-
- Ok(good_ones)
- }
-
- /// Retrieve the download url for a given package.
- pub fn download_url(&self, user: &str, package: &str) -> Result<Url> {
- let path = format!("api/packages/{}/{}/", user, package);
- let endpoint = self
- .base_url
- .join(&path)
- .expect("The parsed path was wrong");
- let response: ContentMeta = ureq::request_url("GET", &endpoint).call()?.into_json()?;
- Ok(response.url)
- }
-}
-
-fn extract_content_id(path: &str) -> Option<ContentId> {
- regex!("/packages/([^/]+)/([^/]+)/$")
- .captures(path)
- .map(|c| {
- (
- c.get(1).unwrap().as_str().into(),
- c.get(2).unwrap().as_str().into(),
- )
- })
-}
diff --git a/src/download.rs b/src/download.rs
deleted file mode 100644
index b9507b7..0000000
--- a/src/download.rs
+++ /dev/null
@@ -1,161 +0,0 @@
-//! Module to download mods from the internet.
-//!
-//! This module allows to download mods from various sources in the internet. Source specification
-//! is done through the [`Source`] enum:
-//!
-//! * [`Source::Http`]: Download straight from an URL. It is expected that the URL points to a zip
-//! archive which contains the mod, either directly or in a subfolder.
-//! * [`Source::ContentDb`]: Refers to a package on the ContentDB. The [`Downloader`] will consult
-//! the API to get the right download URL.
-//! * [`Source::ModId`]: Refers to a simple mod name. Note that this specification can be
-//! ambiguous, in which case the [`Downloader`] will return an error.
-//!
-//! The actual download work is done by a [`Downloader`]. Each [`Downloader`] has its own temporary
-//! directory, in which any mods are downloaded and extracted. If you drop the [`Downloader`],
-//! those mods will be deleted and the objects pointing to the now-gone directories are no longer
-//! useful.
-
-use std::{
- fs,
- io::{Cursor, Read},
- str::FromStr,
-};
-
-use tempdir::TempDir;
-use url::Url;
-use uuid::Uuid;
-use zip::ZipArchive;
-
-use super::{
- contentdb::{ContentDb, ContentId},
- error::{Error, Result},
- minemod::{self, ModContainer, ModId},
-};
-
-/// A source determines where a mod should be loaded from.
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub enum Source {
- /// Download a mod archive through HTTP.
- Http(Url),
- /// Download a mod from the Content DB, using the given user- and package name.
- ContentDb(ContentId),
- /// Search the Content DB for a given mod ID.
- ///
- /// The download may fail if there are multiple mods providing the same ID.
- ModId(ModId),
-}
-
-impl FromStr for Source {
- type Err = Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- if s.starts_with("http://") || s.starts_with("https://") {
- let url = Url::parse(s)?;
- return Ok(Source::Http(url));
- }
- let groups = regex!("^([^/]+)/([^/]+)$").captures(s);
- if let Some(groups) = groups {
- return Ok(Source::ContentDb((
- groups.get(1).unwrap().as_str().into(),
- groups.get(2).unwrap().as_str().into(),
- )));
- }
-
- if !s.contains(' ') {
- return Ok(Source::ModId(s.into()));
- }
-
- Err(Error::InvalidSourceSpec(s.into()))
- }
-}
-
-/// A downloader is responsible for downloading mods from various sources.
-///
-/// Note that the objects that the [`Downloader`] creates will not work after the downloader has
-/// been destroyed, as the temporary files will be lost.
-#[derive(Debug)]
-pub struct Downloader {
- temp_dir: TempDir,
- content_db: ContentDb,
-}
-
-impl Downloader {
- /// Create a new [`Downloader`], refering to the default ContentDB.
- pub fn new() -> Result<Downloader> {
- Downloader::with_content_db(Default::default())
- }
-
- /// Create a new [`Downloader`] that points to a specific ContentDB instance.
- pub fn with_content_db(content_db: ContentDb) -> Result<Downloader> {
- let temp_dir = TempDir::new(env!("CARGO_PKG_NAME"))?;
- Ok(Downloader {
- temp_dir,
- content_db,
- })
- }
-
- /// Download a mod from the given source.
- ///
- /// This function may download either a mod ([`minemod::MineMod`]) or a modpack
- /// ([`minemod::Modpack`]), therefore it returns a trait object that can be queried for the
- /// required information.
- ///
- /// Note that the object will be useless when the [`Downloader`] is dropped, as the temporary
- /// directory containing the downloaded data will be lost. Use [`ModContainer::install_to`] to
- /// copy the mod content to a different directory.
- pub fn download(&self, source: &Source) -> Result<Box<dyn ModContainer>> {
- match *source {
- Source::Http(ref url) => self.download_http(url),
- Source::ContentDb((ref user, ref package)) => {
- let url = self.content_db.download_url(user, package)?;
- self.download_http(&url)
- }
- Source::ModId(ref id) => {
- let candidates = self.content_db.resolve(id)?;
- if candidates.len() != 1 {
- return Err(Error::AmbiguousModId(id.into()));
- }
- self.download_http(&candidates[0].url)
- }
- }
- }
-
- /// Downloads a mod given a HTTP link.
- ///
- /// The [`Downloader`] expects to receive a zipfile containing the mod directory on this link.
- ///
- /// Refer to the module level documentation and [`Downloader::download`] for more information.
- pub fn download_http(&self, url: &Url) -> Result<Box<dyn ModContainer>> {
- let mut reader = ureq::request_url("GET", url).call()?.into_reader();
- let mut data = Vec::new();
- reader.read_to_end(&mut data)?;
- let data = Cursor::new(data);
- let mut archive = ZipArchive::new(data)?;
-
- let dir = self
- .temp_dir
- .path()
- .join(&Uuid::new_v4().to_hyphenated().to_string());
- fs::create_dir(&dir)?;
-
- archive.extract(&dir)?;
-
- // Some archives contain the mod files directly, so try to open it:
- if let Ok(pack) = minemod::open_mod_or_pack(&dir) {
- return Ok(pack);
- }
-
- // If the archive does not contain the mod directly, we instead try the subdirectories that
- // we've extracted.
- for entry in fs::read_dir(&dir)? {
- let entry = entry?;
- let metadata = fs::metadata(&entry.path())?;
- if metadata.is_dir() {
- if let Ok(pack) = minemod::open_mod_or_pack(&entry.path()) {
- return Ok(pack);
- }
- }
- }
- Err(Error::InvalidModDir(dir))
- }
-}
diff --git a/src/error.rs b/src/error.rs
deleted file mode 100644
index 5dbd6b6..0000000
--- a/src/error.rs
+++ /dev/null
@@ -1,64 +0,0 @@
-//! Error definitions for ModderBaas.
-//!
-//! The type alias [`Result`] can be used, which defaults the error type to [`enum@Error`]. Any function
-//! that introduces errors should return a [`Result`] — unless it is clear that a more narrow error
-//! will suffice, such as [`crate::util::copy_recursive`].
-use std::path::PathBuf;
-
-use thiserror::Error;
-
-/// The main error type.
-#[derive(Error, Debug)]
-pub enum Error {
- /// A malformed or otherwise invalid mod ID has been given.
- #[error("invalid mod id '{0}'")]
- InvalidModId(String),
- /// The ContentDB website returned invalid data.
- #[error("the website returned unexpected data")]
- InvalidScrape,
- /// The directory does not contain a valid Minetest mod.
- #[error("'{0}' is not a valid mod directory")]
- InvalidModDir(PathBuf),
- /// The directory does not contain a valid Minetest game.
- #[error("'{0}' is not a valid game directory")]
- InvalidGameDir(PathBuf),
- /// The directory does not contain a valid Minetest world.
- #[error("'{0}' is not a valid world directory")]
- InvalidWorldDir(PathBuf),
- /// The directory does not contain a valid Minetest modpack.
- #[error("'{0}' is not a valid modpack directory")]
- InvalidModpackDir(PathBuf),
- /// The given source string can not be parsed into a [`crate::download::Source`].
- #[error("'{0}' does not represent a valid mod source")]
- InvalidSourceSpec(String),
-
- /// An empty ZIP archive was downloaded.
- #[error("the downloaded file was empty")]
- EmptyArchive,
- /// The given world does not have a game ID set.
- #[error("the world has no game ID set")]
- NoGameSet,
- /// ContentDB returned more than one fitting mod for the query.
- #[error("the mod ID '{0}' does not point to a single mod")]
- AmbiguousModId(String),
-
- /// Wrapper for HTTP errors.
- #[error("underlying HTTP error")]
- UreqError(#[from] ureq::Error),
- /// Wrapper for generic I/O errors.
- #[error("underlying I/O error")]
- IoError(#[from] std::io::Error),
- /// Wrapper for ZIP errors.
- #[error("ZIP error")]
- ZipError(#[from] zip::result::ZipError),
- /// Wrapper for URL parsing errors.
- #[error("invalid URL")]
- UrlError(#[from] url::ParseError),
- /// Wrapper for Unix errors.
- #[cfg(unix)]
- #[error("underlying Unix error")]
- NixError(#[from] nix::errno::Errno),
-}
-
-/// A [`Result`] with the error type defaulted to [`enum@Error`].
-pub type Result<T, E = Error> = std::result::Result<T, E>;
diff --git a/src/game.rs b/src/game.rs
deleted file mode 100644
index 0d2405d..0000000
--- a/src/game.rs
+++ /dev/null
@@ -1,74 +0,0 @@
-//! Module to interact with installed games.
-//!
-//! The main type of this module is a [`Game`], which can be constructed by opening installed
-//! Minetest games through [`Game::open`].
-//!
-//! Valid game directories have a `game.conf` configuration file which contains some metadata about
-//! the game.
-use std::{
- fmt,
- path::{Path, PathBuf},
-};
-
-use super::{
- error::{Error, Result},
- minemod::{self, MineMod},
- scan,
-};
-
-/// Represents an on-disk Minetest game.
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct Game {
- path: PathBuf,
-}
-
-impl Game {
- /// Open the given directory as a Minetest game.
- ///
- /// Note that the path may be relative, but only to the parent directory of the actual game.
- /// This is because Minetest uses the game's directory name to identify the game
- /// ([`Game::technical_name`]), so we need this information.
- pub fn open<P: AsRef<Path>>(path: P) -> Result<Game> {
- Game::open_path(path.as_ref())
- }
-
- fn open_path(path: &Path) -> Result<Game> {
- if path.file_name().is_none() {
- return Err(Error::InvalidGameDir(path.into()));
- }
- let conf = path.join("game.conf");
- if !conf.is_file() {
- return Err(Error::InvalidGameDir(path.into()));
- }
-
- Ok(Game { path: path.into() })
- }
-
- /// Returns the technical name of this game.
- ///
- /// This is the name that is used by minetest to identify the game.
- pub fn technical_name(&self) -> String {
- self.path
- .file_name()
- .expect("Somebody constructed an invalid `Game`")
- .to_str()
- .expect("Non-UTF8 directory encountered")
- .into()
- }
-
- /// Returns all mods that this game provides.
- pub fn mods(&self) -> Result<Vec<MineMod>> {
- let path = self.path.join("mods");
- let mut mods = vec![];
- for container in scan(&path, |p| minemod::open_mod_or_pack(p))? {
- mods.extend(container.mods()?);
- }
- Ok(mods)
- }
-}
-
-impl fmt::Display for Game {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{}", self.technical_name())
- }
-}
diff --git a/src/kvstore.rs b/src/kvstore.rs
deleted file mode 100644
index 006bb94..0000000
--- a/src/kvstore.rs
+++ /dev/null
@@ -1,49 +0,0 @@
-//! Support module for writing `key=value` stores.
-//!
-//! These files are used by minetest mods (`mod.conf`), games (`game.conf`) and worlds
-//! (`world.mt`).
-//!
-//! Key-Value-Stores (KVStores) are represented by a [`std::collections::HashMap`] on the Rust
-//! side.
-use std::{collections::HashMap, fs, io::Write, path::Path};
-
-use super::error::Result;
-
-/// Read the given file as a KVStore.
-pub fn read<P: AsRef<Path>>(path: P) -> Result<HashMap<String, String>> {
- read_inner(path.as_ref())
-}
-
-fn read_inner(path: &Path) -> Result<HashMap<String, String>> {
- let content = fs::read_to_string(path)?;
-
- Ok(content
- .lines()
- .map(|line| line.splitn(2, '=').map(str::trim).collect::<Vec<_>>())
- .map(|v| (v[0].into(), v[1].into()))
- .collect())
-}
-
-/// Write the given KVStore back to the file.
-///
-/// The order of the keys is guaranteed to be the following:
-///
-/// 1. All options that *don't* start with `"load_mod_"` are saved in alphabetical order.
-/// 2. All remaining options are saved in alphabetical order.
-///
-/// Note that this function will **override** existing files!
-pub fn write<P: AsRef<Path>>(data: &HashMap<String, String>, path: P) -> Result<()> {
- write_inner(data, path.as_ref())
-}
-
-fn write_inner(data: &HashMap<String, String>, path: &Path) -> Result<()> {
- let mut items = data.iter().collect::<Vec<_>>();
- items.sort_by_key(|i| (if i.0.starts_with("load_mod_") { 1 } else { 0 }, i.0));
-
- let mut output = fs::File::create(path)?;
- for (key, value) in items {
- writeln!(output, "{} = {}", key, value)?;
- }
-
- Ok(())
-}
diff --git a/src/lib.rs b/src/lib.rs
deleted file mode 100644
index f9b1c8b..0000000
--- a/src/lib.rs
+++ /dev/null
@@ -1,104 +0,0 @@
-//! ModderBaas is a library that allows you to manage Minetest mods.
-//!
-//! The main point of the library is to be used by the `modderbaas` CLI application, however it can
-//! also be used in other Rust programs that want a (limited) API to interact with Minetest
-//! content.
-//!
-//! # Representation
-//!
-//! Most objects ([`Game`], [`World`], [`MineMod`], [`Modpack`]) are represented by the path to the
-//! on-disk directory that contains the corresponding game/world/mod/modpack. The initializers take
-//! care that the directory contains a valid object.
-//!
-//! Many of the query operations do not do any caching, so avoid calling them in tight loops. If
-//! you want to do a lot of name queries (e.g. to find installed mods), consider using a
-//! [`Snapshot`].
-//!
-//! # Racing
-//!
-//! ModderBaas expects that during the time of its access, no other application meddles with the
-//! directories. It will *not* lead to crashes, but you will get a lot more error return values.
-//!
-//! # Mutability
-//!
-//! Some objects support mutating their state. This is usually done by directly writing the data
-//! into the corresponding file. Therefore, those methods (even though they mutate state) only take
-//! a shared reference (`&self`) — keep that in mind!
-//!
-//! # Crate Structure
-//!
-//! Most game objects are implemented in their own module and re-exported at the crate root:
-//!
-//! * [`game`]
-//! * [`minemod`]
-//! * [`world`]
-//!
-//! Interacting with mods from the internet/ContentDB is done through two modules:
-//!
-//! * [`contentdb`]
-//! * [`download`]
-//!
-//! Some modules contain auxiliary functions:
-//!
-//! * [`kvstore`]
-//! * [`util`]
-//!
-//! Error definitions are in [`error`].
-//!
-//! [`baas`] contains functions and data to interact with the global Minetest installation.
-use std::{fs, path::Path};
-
-use log::debug;
-
-macro_rules! regex {
- ($re:literal $(,)?) => {{
- static RE: once_cell::sync::OnceCell<regex::Regex> = once_cell::sync::OnceCell::new();
- RE.get_or_init(|| regex::Regex::new($re).unwrap())
- }};
-}
-
-pub mod baas;
-pub mod contentdb;
-pub mod download;
-pub mod error;
-pub mod game;
-pub mod kvstore;
-pub mod minemod;
-pub mod util;
-pub mod world;
-
-pub use baas::{Baas, Snapshot};
-pub use contentdb::ContentDb;
-pub use download::{Downloader, Source};
-pub use error::Error;
-pub use game::Game;
-pub use minemod::{MineMod, Modpack};
-pub use world::World;
-
-use error::Result;
-
-/// Scan all files in the given directory.
-///
-/// Files for which `scanner` returns `Ok(..)` will be collected and returned. Files for which
-/// `scanner` returns `Err(..)` will be silently discarded.
-///
-/// This function is useful to iterate through the items in a directory and find fitting objects:
-///
-/// ```rust
-/// use modderbaas::minemod::MineMod;
-/// let mods = scan("/tmp", |p| MineMod::open(p))?;
-/// ```
-pub fn scan<W, P: AsRef<Path>, F: for<'p> Fn(&'p Path) -> Result<W>>(
- path: P,
- scanner: F,
-) -> Result<Vec<W>> {
- debug!("Scanning through {:?}", path.as_ref());
- let mut good_ones = vec![];
- for entry in fs::read_dir(path)? {
- let entry = entry?;
- if let Ok(i) = scanner(&entry.path()) {
- good_ones.push(i);
- }
- }
- Ok(good_ones)
-}
diff --git a/src/minemod.rs b/src/minemod.rs
deleted file mode 100644
index 456d1c6..0000000
--- a/src/minemod.rs
+++ /dev/null
@@ -1,242 +0,0 @@
-//! Module to interact with installed mods and modpacks.
-//!
-//! Due to technical reasons (`mod` being a Rust keyword), this module is called `minemod` and the
-//! mod objects are called [`MineMod`].
-//!
-//! Simple mods are represented by [`MineMod`], which can be opened by [`MineMod::open`]. Valid
-//! mods are identified by their `mod.conf`, which contains metadata about the mod.
-//!
-//! Modpacks can be represented by [`Modpack`] and loaded through [`Modpack::open`]. A modpack is
-//! just a collection of mods grouped together, and the modpack directory needs to have a
-//! `modpack.conf` in its directory.
-//!
-//! # Mods and Packs United
-//!
-//! In some cases, we cannot know in advance whether we are dealing with a mod or a modpack (e.g.
-//! when downloading content from ContentDB). Therefore, the trait [`ModContainer`] exists, which
-//! can be used as a trait object (`Box<dyn ModContainer>`). It provides the most important methods
-//! and allows downcasting through `Any`.
-//!
-//! If you want to work with the mods directly, you can use [`ModContainer::mods`], which returns
-//! the mod itself for [`MineMod`]s, and all contained mods for [`Modpack`]s.
-//!
-//! If you want to open a directory as a [`ModContainer`], you can use [`open_mod_or_pack`].
-
-use std::{
- any::Any,
- collections::HashMap,
- fmt, fs,
- path::{Path, PathBuf},
-};
-
-use super::{
- error::{Error, Result},
- kvstore, scan, util,
-};
-
-/// The type of the ID that is used to identify Minetest mods.
-pub type ModId = String;
-
-/// A minemod is a mod that is saved somewhere on disk.
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct MineMod {
- path: PathBuf,
-}
-
-impl MineMod {
- /// Opens the given directory as a mod.
- pub fn open<P: AsRef<Path>>(path: P) -> Result<MineMod> {
- MineMod::open_path(path.as_ref())
- }
-
- fn open_path(path: &Path) -> Result<MineMod> {
- let conf = path.join("mod.conf");
- if !conf.is_file() {
- return Err(Error::InvalidModDir(path.into()));
- }
-
- Ok(MineMod { path: path.into() })
- }
-
- /// Returns the path of this mod.
- pub fn path(&self) -> &Path {
- &self.path
- }
-
- fn read_conf(&self) -> Result<HashMap<String, String>> {
- let conf = self.path.join("mod.conf");
- kvstore::read(&conf)
- }
-
- /// Read the mod ID.
- pub fn mod_id(&self) -> Result<ModId> {
- let conf = self.read_conf()?;
- conf.get("name")
- .map(Into::into)
- .ok_or_else(|| Error::InvalidModDir(self.path.clone()))
- }
-
- /// Returns all dependencies of this mod.
- pub fn dependencies(&self) -> Result<Vec<ModId>> {
- let conf = self.read_conf()?;
- static EMPTY: String = String::new();
- let depstr = conf.get("depends").unwrap_or(&EMPTY);
- Ok(depstr
- .split(',')
- .map(str::trim)
- .filter(|s| !s.is_empty())
- .map(Into::into)
- .collect())
- }
-
- /// Copies the mod to the given path.
- ///
- /// Note that the path should not include the mod directory, that will be appended
- /// automatically.
- ///
- /// Returns a new [`MineMod`] object pointing to the copy.
- pub fn copy_to<P: AsRef<Path>>(&self, path: P) -> Result<MineMod> {
- let path = path.as_ref().join(self.mod_id()?);
- fs::create_dir_all(&path)?;
- util::copy_recursive(&self.path, &path)?;
- MineMod::open(&path)
- }
-}
-
-impl fmt::Display for MineMod {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{}", self.mod_id().map_err(|_| fmt::Error)?)
- }
-}
-
-/// Represents an on-disk modpack.
-///
-/// We don't support many modpack operations besides listing the modpack contents.
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct Modpack {
- path: PathBuf,
-}
-
-impl Modpack {
- /// Opens the given directory as a modpack.
- pub fn open<P: AsRef<Path>>(path: P) -> Result<Modpack> {
- Modpack::open_path(path.as_ref())
- }
-
- fn open_path(path: &Path) -> Result<Modpack> {
- let conf = path.join("modpack.conf");
- if !conf.is_file() {
- return Err(Error::InvalidModpackDir(path.into()));
- }
-
- Ok(Modpack { path: path.into() })
- }
-
- /// Returns the path of this modpack.
- pub fn path(&self) -> &Path {
- &self.path
- }
-
- fn conf(&self) -> Result<HashMap<String, String>> {
- let conf = self.path.join("modpack.conf");
- kvstore::read(&conf)
- }
-
- /// Returns the name of the modpack.
- pub fn name(&self) -> Result<String> {
- self.conf()?
- .get("name")
- .map(Into::into)
- .ok_or_else(|| Error::InvalidModDir(self.path.clone()))
- }
-
- /// Return all mods contained in this modpack.
- pub fn mods(&self) -> Result<Vec<MineMod>> {
- let mut mods = vec![];
- for container in scan(&self.path, |p| open_mod_or_pack(p))? {
- mods.extend(container.mods()?);
- }
- Ok(mods)
- }
-
- /// Copies the modpack to the given path.
- ///
- /// Note that the path should not include the modpack directory, that will be appended
- /// automatically.
- ///
- /// Returns a new [`Modpack`] object pointing to the copy.
- pub fn copy_to<P: AsRef<Path>>(&self, path: P) -> Result<Modpack> {
- let path = path.as_ref().join(self.name()?);
- fs::create_dir_all(&path)?;
- util::copy_recursive(&self.path, &path)?;
- Modpack::open(&path)
- }
-}
-
-impl fmt::Display for Modpack {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{} (pack)", self.name().map_err(|_| fmt::Error)?)
- }
-}
-
-/// A thing that can contain mods.
-///
-/// This is useful for code that should deal with both mods and modpacks.
-pub trait ModContainer: Any + fmt::Display {
- /// Returns the name of the mod container.
- fn name(&self) -> Result<String>;
-
- /// Returns the on-disk path of this mod container.
- fn path(&self) -> &Path;
-
- /// Return all contained mods.
- fn mods(&self) -> Result<Vec<MineMod>>;
-
- /// Copies the content to the given directory.
- fn install_to(&self, path: &Path) -> Result<Box<dyn ModContainer>>;
-}
-
-impl ModContainer for MineMod {
- fn name(&self) -> Result<String> {
- self.mod_id()
- }
-
- fn path(&self) -> &Path {
- self.path()
- }
-
- fn mods(&self) -> Result<Vec<MineMod>> {
- Ok(vec![self.clone()])
- }
-
- fn install_to(&self, path: &Path) -> Result<Box<dyn ModContainer>> {
- self.copy_to(path)
- .map(|x| Box::new(x) as Box<dyn ModContainer>)
- }
-}
-
-impl ModContainer for Modpack {
- fn name(&self) -> Result<String> {
- self.name()
- }
-
- fn path(&self) -> &Path {
- self.path()
- }
-
- fn mods(&self) -> Result<Vec<MineMod>> {
- self.mods()
- }
-
- fn install_to(&self, path: &Path) -> Result<Box<dyn ModContainer>> {
- self.copy_to(path)
- .map(|x| Box::new(x) as Box<dyn ModContainer>)
- }
-}
-
-/// Attempts to open the given path as either a single mod or a modpack.
-pub fn open_mod_or_pack<P: AsRef<Path>>(path: P) -> Result<Box<dyn ModContainer>> {
- MineMod::open(path.as_ref())
- .map(|m| Box::new(m) as Box<dyn ModContainer>)
- .or_else(|_| Modpack::open(path.as_ref()).map(|p| Box::new(p) as Box<dyn ModContainer>))
-}
diff --git a/src/util.rs b/src/util.rs
deleted file mode 100644
index ea401ba..0000000
--- a/src/util.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-//! Utility functions.
-use std::{fs, io, path::Path};
-
-#[cfg(unix)]
-use nix::unistd::{self, Gid, Uid};
-
-use super::error::Result;
-
-#[cfg(not(unix))]
-pub mod nix {
- //! Stub mod on non-unix systems.
- pub mod unistd {
- pub enum Uid {}
- pub enum Gid {}
- }
-}
-
-/// Recursively copy the *contents* of the given directory to the given path.
-pub fn copy_recursive<S: AsRef<Path>, D: AsRef<Path>>(source: S, destination: D) -> io::Result<()> {
- copy_inner(source.as_ref(), destination.as_ref())
-}
-
-fn copy_inner(source: &Path, destination: &Path) -> io::Result<()> {
- for item in fs::read_dir(source)? {
- let item = item?;
- let metadata = item.metadata()?;
- let item_destination = destination.join(item.file_name());
- if metadata.is_file() {
- fs::copy(&item.path(), &item_destination)?;
- } else if metadata.is_dir() {
- fs::create_dir(&item_destination)?;
- copy_inner(&item.path(), &item_destination)?;
- }
- }
- Ok(())
-}
-
-/// Recursively change the owner of the given path to the given ones.
-///
-/// Note that this function only works on Unix systems. **It will panic on other systems!**
-pub fn chown_recursive<P: AsRef<Path>>(path: P, uid: Option<Uid>, gid: Option<Gid>) -> Result<()> {
- chown_inner(path.as_ref(), uid, gid)
-}
-
-#[cfg(unix)]
-fn chown_inner(path: &Path, uid: Option<Uid>, gid: Option<Gid>) -> Result<()> {
- unistd::chown(path, uid, gid)?;
-
- let metadata = fs::metadata(path)?;
- if metadata.is_dir() {
- for item in fs::read_dir(path)? {
- let item = item?;
- chown_inner(&item.path(), uid, gid)?;
- }
- }
- Ok(())
-}
-
-#[cfg(not(unix))]
-fn chown_inner(_: &Path, _: Option<Uid>, _: Option<Gid>) -> Result<()> {
- panic!("chown() is not available on non-Unix systems!");
-}
diff --git a/src/world.rs b/src/world.rs
deleted file mode 100644
index 5dce6d0..0000000
--- a/src/world.rs
+++ /dev/null
@@ -1,97 +0,0 @@
-//! Module to interact with Minetest worlds (savegames).
-//!
-//! The main object is [`World`], which represents a Minetest world on-disk.
-use std::{
- collections::HashMap,
- fmt,
- path::{Path, PathBuf},
-};
-
-use super::{
- error::{Error, Result},
- kvstore,
- minemod::ModId,
-};
-
-/// Represents an on-disk Minetest world.
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct World {
- path: PathBuf,
-}
-
-impl World {
- /// Open the given directory as a [`World`].
- pub fn open<P: AsRef<Path>>(path: P) -> Result<World> {
- World::open_path(path.as_ref())
- }
-
- fn open_path(path: &Path) -> Result<World> {
- let conf = path.join("world.mt");
- if !conf.is_file() {
- return Err(Error::InvalidWorldDir(path.into()));
- }
-
- Ok(World { path: path.into() })
- }
-
- fn conf(&self) -> Result<HashMap<String, String>> {
- let conf = self.path.join("world.mt");
- kvstore::read(&conf)
- }
-
- /// Returns the name of the world.
- pub fn world_name(&self) -> Result<String> {
- let fallback = self
- .path
- .file_name()
- .map(|s| s.to_str().expect("Non-UTF8 directory encountered"));
-
- let conf = self.conf()?;
- conf.get("world_name")
- .map(String::as_str)
- .or(fallback)
- .ok_or_else(|| Error::InvalidWorldDir(self.path.clone()))
- .map(Into::into)
- }
-
- /// Extract the game that this world uses.
- pub fn game_id(&self) -> Result<String> {
- let conf = self.conf()?;
- conf.get("gameid").ok_or(Error::NoGameSet).map(Into::into)
- }
-
- /// Returns all mods that are loaded in this world.
- ///
- /// This returns mods that are explicitely loaded in the config, but not mods that are loaded
- /// through the game.
- pub fn mods(&self) -> Result<Vec<ModId>> {
- let conf = self.conf()?;
- const PREFIX_LEN: usize = "load_mod_".len();
- Ok(conf
- .iter()
- .filter(|(k, _)| k.starts_with("load_mod_"))
- .filter(|(_, v)| *v == "true")
- .map(|i| i.0[PREFIX_LEN..].into())
- .collect())
- }
-
- /// Enable the given mod.
- ///
- /// Note that this function does not ensure that the mod exists on-disk, nor does it do any
- /// dependency checks. It simply adds the right `load_mod`-line to the world configuration
- /// file.
- pub fn enable_mod(&self, mod_id: &str) -> Result<()> {
- let mut conf = self.conf()?;
- let key = format!("load_mod_{}", mod_id);
- conf.entry(key)
- .and_modify(|e| *e = "true".into())
- .or_insert_with(|| "true".into());
- kvstore::write(&conf, &self.path.join("world.mt"))
- }
-}
-
-impl fmt::Display for World {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{}", self.world_name().map_err(|_| fmt::Error)?)
- }
-}