1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
// UPGRADE.rs
// by Lut99
//
// Created:
// 03 Oct 2023, 10:52:44
// Last edited:
// 03 Oct 2023, 11:30:53
// Auto updated?
// Yes
//
// Description:
//! Implements functions for upgrading previous version configuration
//! files to the newer ones.
//
use std::borrow::Cow;
use std::error;
use std::fmt::{Display, Formatter, Result as FResult};
use std::fs::{self, DirEntry};
use std::path::{Path, PathBuf};
use console::style;
use log::{debug, info, warn};
use serde::Serialize;
use specifications::version::Version;
use crate::old_configs::v1_0_0;
use crate::spec::VersionFix;
/***** CONSTANTS *****/
/// The maximum length of files we consider.
const MAX_FILE_LEN: u64 = 1024 * 1024;
/***** TYPE ALIASES *****/
/// Alias for the closure that parses according to a particular version number.
///
/// Note that this closure returns another closure, which converts the parsed value into an up-to-date version of the file.
type VersionParser<'f1, 'f2, T> = Box<dyn 'f1 + Fn(&str) -> Option<VersionConverter<'f2, T>>>;
/// Alias for the closure that takes a parsed file and converts it to an up-to-date version of the file.
type VersionConverter<'f, T> = Box<dyn 'f + FnOnce(&Path, bool) -> Result<T, Error>>;
/***** ERRORS *****/
/// Describes errors that may occur when upgrading config files.
#[derive(Debug)]
pub enum Error {
/// Failed to request some input not provided by older files.
Input { what: &'static str, err: brane_shr::input::Error },
/// The given path was not found.
PathNotFound { path: PathBuf },
/// Failed to read a directory.
DirRead { path: PathBuf, err: std::io::Error },
/// Failed to read an entry within a directory.
DirEntryRead { path: PathBuf, entry: usize, err: std::io::Error },
/// Failed to read a file.
FileRead { path: PathBuf, err: std::io::Error },
/// Failed to read the metadata of a file.
FileMetadataRead { path: PathBuf, err: std::io::Error },
/// Failed to convert between the infos
Convert { what: &'static str, version: Version, err: Box<dyn error::Error> },
/// Failed to serialize the new info.
Serialize { what: &'static str, err: serde_yaml::Error },
/// Failed to create a new file.
FileWrite { path: PathBuf, err: std::io::Error },
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> FResult {
use Error::*;
match self {
Input { what, .. } => write!(f, "Failed to query the user (you!) for a {what}"),
PathNotFound { path } => write!(f, "Path '{}' not found", path.display()),
DirRead { path, .. } => write!(f, "Failed to read directory '{}'", path.display()),
DirEntryRead { path, entry, .. } => write!(f, "Failed to read entry {} in directory '{}'", entry, path.display()),
FileRead { path, .. } => write!(f, "Failed to read from file '{}'", path.display()),
FileMetadataRead { path, .. } => write!(f, "Failed to read metadata of file '{}'", path.display()),
Serialize { what, .. } => write!(f, "Failed to serialize upgraded {what} file"),
FileWrite { path, .. } => write!(f, "Failed to write to file '{}'", path.display()),
Convert { what, version, .. } => write!(f, "Failed to convert v{} {} to v{}", version, what, env!("CARGO_PKG_VERSION")),
}
}
}
impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
use Error::*;
match self {
Input { err, .. } => Some(err),
PathNotFound { .. } => None,
DirRead { err, .. } => Some(err),
DirEntryRead { err, .. } => Some(err),
FileRead { err, .. } => Some(err),
FileMetadataRead { err, .. } => Some(err),
Serialize { err, .. } => Some(err),
FileWrite { err, .. } => Some(err),
Convert { err, .. } => Some(&**err),
}
}
}
/***** HELPER FUNCTIONS *****/
/// Does the heavy lifting in this module by implementing the iteration and trying to upgrade.
///
/// # Arguments
/// - `what`: Some debug-only string that is used to describe the kind of file we are upgrading (e.g., `node.yml`).
/// - `path`: The path fo the file or folder (to scour for files) to upgrade.
/// - `versions`: An ordered list of old BRANE version numbers to closures implementing a parser and a converter, respectively. The parsers are tried in-order.
/// - `dry_run`: Whether to only report which files to upgrade, instead of upgrading them.
/// - `overwrite`: Whether to overwrite the files instead of creating new ones.
///
/// # Errors
/// This function may error if we failed to read from disk.
fn upgrade<T: Serialize>(
what: &'static str,
path: impl Into<PathBuf>,
versions: Vec<(Version, VersionParser<T>)>,
dry_run: bool,
overwrite: bool,
) -> Result<(), Error> {
// Create a queue to parse
let mut todo: Vec<PathBuf> = vec![path.into()];
while let Some(path) = todo.pop() {
debug!("Examining '{}'", path.display());
// Switch on the type of path
if path.is_file() {
debug!("Path '{}' points to a file", path.display());
// Check if the file is not _too_ large
match fs::metadata(&path) {
Ok(metadata) => {
if metadata.len() >= MAX_FILE_LEN {
debug!("Ignoring '{}', since the file is too large (>= {} bytes)", path.display(), MAX_FILE_LEN);
continue;
}
},
Err(err) => {
return Err(Error::FileMetadataRead { path, err });
},
};
// Read the file
let raw: Vec<u8> = match fs::read(&path) {
Ok(raw) => raw,
Err(err) => {
return Err(Error::FileRead { path, err });
},
};
// Note that non-UTF-8 files are OK, we just ignore them
let raw: String = match String::from_utf8(raw) {
Ok(raw) => raw,
Err(err) => {
debug!("Ignoring '{}', since the file contains invalid UTF-8 ({})", path.display(), err);
continue;
},
};
// Attempt to parse it with any of the valid files
for (version, parser) in &versions {
debug!("Attempting to parse '{}' as v{} {} file...", path.display(), version, what);
// Attempt to parse the string
if let Some(converter) = parser(&raw) {
debug!("File '{}' is a v{} {} file", path.display(), version, what);
// Convert it to another file
let parent: Cow<Path> = path
.parent()
.map(Cow::Borrowed)
.unwrap_or_else(|| if path.is_absolute() { Cow::Owned("/".into()) } else { Cow::Owned("./".into()) });
if !dry_run && overwrite {
// We upgrade in-place
println!(
"Upgrading file {} from {} to {}...",
style(path.display()).green().bold(),
style(format!("v{version}")).bold(),
style(format!("v{}", env!("CARGO_PKG_VERSION"))).bold()
);
// Run the upgrade and serialize the resulting file
debug!("Converting file...");
let new_info: T = converter(parent.as_ref(), true)?;
let new_info: String = match serde_yaml::to_string(&new_info) {
Ok(info) => info,
Err(err) => {
return Err(Error::Serialize { what, err });
},
};
// Write the string to the file no sweat
debug!("Writing file to '{}'...", path.display());
if let Err(err) = fs::write(&path, new_info) {
return Err(Error::FileWrite { path, err });
}
debug!("File '{}' successfully upgraded", path.display());
} else if !dry_run && !overwrite {
// We upgrade to a new location
let new_path: PathBuf = path.with_extension(format!(".yml.{}", env!("CARGO_PKG_VERSION")));
println!(
"Upgrading file {} to {}, from {} to {}...",
style(path.display()).green().bold(),
style(new_path.display()).green().bold(),
style(format!("v{version}")).bold(),
style(format!("v{}", env!("CARGO_PKG_VERSION"))).bold()
);
// Run the upgrade and serialize the resulting file
debug!("Converting file...");
let new_info: T = converter(parent.as_ref(), false)?;
let new_info: String = match serde_yaml::to_string(&new_info) {
Ok(info) => info,
Err(err) => {
return Err(Error::Serialize { what, err });
},
};
// Write the string to the file no sweat
debug!("Writing file to '{}'...", new_path.display());
if let Err(err) = fs::write(&new_path, new_info) {
return Err(Error::FileWrite { path: new_path, err });
}
debug!("File '{}' successfully upgraded", path.display());
} else {
// We don't upgrade, just notify
println!(
"Found {} {} file that is candidate for upgrading: {}",
style(format!("v{version}")).bold(),
style(what).bold(),
style(path.display()).green().bold()
);
}
}
}
} else if path.is_dir() {
debug!("Path '{}' points to a directory", path.display());
// Collect the entries of this directory and recurse into that
debug!("Recursing into directory entries...");
match fs::read_dir(&path) {
Ok(entries) => {
for (i, entry) in entries.enumerate() {
// Unwrap the entry
let entry: DirEntry = match entry {
Ok(entry) => entry,
Err(err) => {
return Err(Error::DirEntryRead { path, entry: i, err });
},
};
// Add its path to the queue
if todo.len() == todo.capacity() {
todo.reserve(todo.len());
}
todo.push(entry.path());
}
},
Err(err) => {
return Err(Error::DirRead { path, err });
},
}
// Continue with the next one
} else if !path.exists() {
return Err(Error::PathNotFound { path });
} else {
warn!("Given path '{}' is a non-file, non-directory path (skipping)", path.display());
continue;
}
}
// Done, we've converted all files
Ok(())
}
/***** LIBRARY *****/
/// Converts old-style `data.yml` files to new-style ones.
///
/// # Arguments
/// - `path`: The path fo the file or folder (to scour for files) to upgrade.
/// - `dry_run`: Whether to only report which files to upgrade, instead of upgrading them.
/// - `overwrite`: Whether to overwrite the files instead of creating new ones.
/// - `version`: Whether to only consider files that are in a particular BRANE version.
///
/// # Errors
/// This function may error if we failed to read from disk.
pub fn data(path: impl Into<PathBuf>, dry_run: bool, overwrite: bool, version: VersionFix) -> Result<(), Error> {
use specifications::data::{AccessKind, DataInfo};
use v1_0_0::data as v1_0_0;
let path: PathBuf = path.into();
info!("Upgrading data.yml files in '{}'...", path.display());
// Construct the list of versions
let mut versions: Vec<(Version, VersionParser<DataInfo>)> = vec![(
Version::new(1, 0, 0),
Box::new(|raw: &str| -> Option<VersionConverter<DataInfo>> {
// Attempt to read it with the file
let cfg: v1_0_0::DataInfo = match serde_yaml::from_str(raw) {
Ok(cfg) => cfg,
Err(_) => {
return None;
},
};
// Return a function for converting it to a new-style function
Some(Box::new(move |_dir: &Path, _overwrite: bool| -> Result<DataInfo, Error> {
// No need to write additional files, so we can ignore the input
// Convert each of the contents
Ok(DataInfo {
name: cfg.name,
owners: cfg.owners,
description: cfg.description,
created: cfg.created,
access: cfg
.access
.into_iter()
.map(|(loc, access)| {
(loc, match access {
v1_0_0::AccessKind::File { path } => AccessKind::File { path },
})
})
.collect(),
})
}))
}),
)];
// Limit the version to only the given one if applicable
if let Some(version) = version.0 {
versions.retain(|(v, _)| v == &version);
}
// Call the function that does the heavy lifting
upgrade::<DataInfo>("data.yml", path, versions, dry_run, overwrite)
}