.
This commit is contained in:
FantasyPvP
2024-03-19 00:31:11 +00:00
parent af021a7cd1
commit b1107a7973
4 changed files with 303 additions and 178 deletions
Generated
+11
View File
@@ -102,6 +102,16 @@ dependencies = [
"inout", "inout",
] ]
[[package]]
name = "colored"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8"
dependencies = [
"lazy_static",
"windows-sys",
]
[[package]] [[package]]
name = "constant_time_eq" name = "constant_time_eq"
version = "0.1.5" version = "0.1.5"
@@ -392,6 +402,7 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
name = "pack_sync" name = "pack_sync"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"colored",
"dirs", "dirs",
"num_cpus", "num_cpus",
"serde", "serde",
+1
View File
@@ -14,6 +14,7 @@ path = "src/server.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
colored = "2.1.0"
dirs = "5.0.1" dirs = "5.0.1"
num_cpus = "1.16.0" num_cpus = "1.16.0"
serde = { version = "1.0.188", features = ["derive"] } serde = { version = "1.0.188", features = ["derive"] }
+118
View File
@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="ALL" />
</component>
<component name="CargoProjects">
<cargoProject FILE="$PROJECT_DIR$/Cargo.toml" />
</component>
<component name="ChangeListManager">
<list default="true" id="abd7e2a1-6cc2-4e84-8a62-99f4db266117" name="Changes" comment="started v2" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Rust File" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="MacroExpansionManager">
<option name="directoryName" value="bu99nprz" />
</component>
<component name="MarkdownSettingsMigration">
<option name="stateVersion" value="1" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 5
}</component>
<component name="ProjectId" id="2dptEi899ygNDxZC2ozxKb1Nk3N" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;ASKED_SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;,
&quot;Cargo.Run Cli.executor&quot;: &quot;Run&quot;,
&quot;Cargo.Run.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.rust.reset.selective.auto.import&quot;: &quot;true&quot;,
&quot;SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;last_opened_file_path&quot;: &quot;/home/fantasypvp/Everything else/packs/_pack_sync/_v2&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;org.rust.cargo.project.model.PROJECT_DISCOVERY&quot;: &quot;true&quot;,
&quot;org.rust.cargo.project.model.impl.CargoExternalSystemProjectAware.subscribe.first.balloon&quot;: &quot;&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;preferences.lookFeel&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}</component>
<component name="RunManager">
<configuration name="Run Cli" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="command" value="run --bin pack_sync_cli" />
<option name="workingDirectory" value="file://$PROJECT_DIR$" />
<envs />
<option name="emulateTerminal" value="true" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" />
<method v="2">
<option name="CARGO.BUILD_TASK_PROVIDER" enabled="true" />
</method>
</configuration>
</component>
<component name="RustProjectSettings">
<option name="toolchainHomeDirectory" value="$USER_HOME$/.cargo/bin" />
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="abd7e2a1-6cc2-4e84-8a62-99f4db266117" name="Changes" comment="" />
<created>1710716737984</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1710716737984</updated>
<workItem from="1710716739016" duration="9374000" />
<workItem from="1710756547640" duration="752000" />
<workItem from="1710757385945" duration="1652000" />
<workItem from="1710780545183" duration="8034000" />
<workItem from="1710790877242" duration="489000" />
<workItem from="1710795982034" duration="1263000" />
<workItem from="1710803219597" duration="109000" />
</task>
<task id="LOCAL-00001" summary="started v2">
<option name="closed" value="true" />
<created>1710759220133</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1710759220133</updated>
</task>
<option name="localTasksCounter" value="2" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="started v2" />
<option name="LAST_COMMIT_MESSAGE" value="started v2" />
</component>
</project>
+155 -160
View File
@@ -1,13 +1,13 @@
use num_cpus; use num_cpus;
use std::{ use std::{
fs::{self, OpenOptions}, fs::{self, OpenOptions},
io::{self, Error, ErrorKind, Write}, io::{self, Error, ErrorKind, Write, Read},
path::{Path, PathBuf}, path::{Path, PathBuf},
collections::HashMap,
slice::SliceIndex
}; };
use std::collections::HashMap; use colored::Colorize;
use std::slice::SliceIndex;
use tracing::{event, info, span, Level, warn, error}; use tracing::{event, info, span, Level, warn, error};
use tracing_subscriber::{ use tracing_subscriber::{
filter::{EnvFilter, LevelFilter}, filter::{EnvFilter, LevelFilter},
@@ -16,10 +16,11 @@ use tracing_subscriber::{
}; };
use toml; use toml;
use walkdir::WalkDir; use walkdir::WalkDir;
use zip::write::{FileOptions, ZipWriter}; use zip::{
use zip::CompressionMethod; CompressionMethod, ZipArchive,
write::{FileOptions, ZipWriter}
};
use zip_archive::{get_dir_list, Archiver}; use zip_archive::{get_dir_list, Archiver};
@@ -36,7 +37,7 @@ fn main() {
) )
.with( .with(
EnvFilter::builder() EnvFilter::builder()
.with_default_directive(LevelFilter::DEBUG.into()) .with_default_directive(LevelFilter::WARN.into())
.from_env_lossy(), .from_env_lossy(),
) )
.init(); .init();
@@ -54,11 +55,27 @@ fn clear_resourcepacks(conf: &Config) -> io::Result<()> {
let span = span!(Level::INFO, "clear_packs"); let span = span!(Level::INFO, "clear_packs");
let _guard = span.enter(); let _guard = span.enter();
info!("Clearing existing resourcepacks folder"); conf.versions.iter().for_each(|(k, v)| {
if let Some(folder) = v.resourcepacks.clone() {
match fs::remove_dir_all(&folder) {
Ok(_) => {}
Err(e) => warn!("No existing packs folder detected for version. Creating new folder."),
}
match fs::create_dir(&folder) {
Ok(_) => {}
Err(e) => {
error!("{}", e);
return;
}
};
}
});
match fs::remove_dir_all(&conf.resourcepacks) { match fs::remove_dir_all(&conf.resourcepacks) {
Ok(_) => {} Ok(_) => {}
Err(e) => warn!("No existing packs folder detected. Creating new folder."), Err(e) => warn!("No existing global packs folder detected. Creating new folder."),
}; };
match fs::create_dir(&conf.resourcepacks) { match fs::create_dir(&conf.resourcepacks) {
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
@@ -66,35 +83,62 @@ fn clear_resourcepacks(conf: &Config) -> io::Result<()> {
return Err(e); return Err(e);
} }
}; };
info!("Successfully reset resourcepacks folder");
Ok(()) Ok(())
} }
fn search_dir(path: &PathBuf, config: &Config) -> io::Result<()> { fn search_dir(path: &PathBuf, config: &Config) -> io::Result<()> {
let span = span!(Level::INFO, "Searching for packs"); let span = span!(Level::INFO, "Searching");
let _guard = span.enter(); let _guard = span.enter();
// info!("Searching for packs in {}", path.display()); // info!("Searching for packs in {}", path.display());
// check if file is a valid pack // check if file is a valid pack
if path.clone().join("pack.toml").exists() { println!("{}", "\nArchiving Packs...".blue().underline());
println!("Found pack {}", path.display()); WalkDir::new(path.clone())
Pack::load(&path).unwrap().archive(&path, config).unwrap();
return Ok(());
} else {
println!("1c| {}", path.display());
for entry in WalkDir::new(path.clone())
.into_iter() .into_iter()
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
.filter(|e| e.file_type().is_dir() && e.file_name() != path) .filter(|e| !e.path().to_str().unwrap().contains("/_"))
.filter(|e| !e.file_name().to_str().unwrap().to_string().contains("/_")) .filter(|e| e.file_name().to_str().unwrap() == "pack.toml")
{ .for_each(|entry| {
println!("PATH {}", entry.path().join(entry.file_name()).to_str().unwrap()); if let Ok(pack) = Pack::from_path(&entry.path().to_path_buf()) {
// search_dir(&entry.path().join(entry.file_name()).to_path_buf(), config).unwrap(); println!(" {} {} {} : {}", "̯".blue(), "Found".green(), entry.path().to_str().unwrap(), pack.name);
} pack.archive( &mut entry.path().to_path_buf(), config).unwrap();
} }
});
println!("{}", " ȯ Done!".blue());
println!("{}", "\nChecking for existing zip files...".blue().underline());
WalkDir::new(path.clone())
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| !e.path().to_str().unwrap().contains("/_"))
.filter(|e| e.file_name().to_str().unwrap().ends_with(".zip"))
.for_each(|entry| {
if let Ok(file) = std::fs::File::open(&entry.path().to_path_buf()) {
if let Ok(mut archive) = ZipArchive::new(file) {
if let Ok(mut pack_config) = archive.by_name("pack.toml") {
if let Ok(pack) = Pack::from_string({
let mut buff = String::new();
pack_config.read_to_string(&mut buff).unwrap();
buff
}) {
println!(" {} {} {} : {}", "̯".blue(), "Found".green(), entry.path().display(), pack.name);
if pack.release {
pack.move_archive(&mut entry.path().to_path_buf(), config).unwrap()
}
}
} else {
println!(" {} {} {}", "̯".blue(), "Found".green(), entry.path().display());
if !fs::read_dir(&config.resourcepacks).unwrap().any(|f| f.unwrap().file_name().to_str().unwrap() == format!("{}.zip", entry.file_name().to_str().unwrap()).as_str()) {
fs::copy(entry.path(), format!("{}/{}", &config.resourcepacks, entry.file_name().to_str().unwrap())).unwrap();
} else {
println!(" {} {}", "".blue(), " File Already Exists: Ignoring".yellow());
}
}
}
}
});
println!("{}", " ȯ Done!".blue());
Ok(()) Ok(())
} }
@@ -107,33 +151,39 @@ struct Pack {
name: String, name: String,
version: String, version: String,
description: String, description: String,
packs: HashMap<String, PackComponent>, packs: Option<HashMap<String, PackComponent>>,
} }
impl Pack { impl Pack {
fn load(path: &PathBuf) -> io::Result<Pack> { fn from_path(path: &PathBuf) -> io::Result<Pack> {
let pack_config = fs::read_to_string(path).unwrap();
Pack::from_string(pack_config)
}
fn from_string(pack_config: String) -> io::Result<Pack> {
let span = span!(Level::INFO, "Loading Pack Config file"); let span = span!(Level::INFO, "Loading Pack Config file");
let _guard = span.enter(); let _guard = span.enter();
let contents = fs::read_to_string(path.join("pack.toml"))?; toml::from_str::<Pack>(&pack_config).map_err(|e| Error::new(ErrorKind::Other, e))
let pack = toml::from_str::<Pack>(&contents).map_err(|e| Error::new(ErrorKind::Other, e))?;
Ok(pack)
} }
fn archive(&self, path: &PathBuf, config: &Config) -> Result<(), Box<dyn std::error::Error>> { fn archive(
let span = span!(Level::INFO, "Archiving Pack"); &self, // pack config
path: &mut PathBuf, // the path to the pack in the filesystem
config: &Config // global config for pack sync
) -> Result<(), Box<dyn std::error::Error>> {
let span = span!(Level::INFO, "Archiving Pack ");
let _guard = span.enter(); let _guard = span.enter();
let resourcepacks = if config.versions.contains_key(&self.version) { path.pop();
match config.versions.get(&self.version).unwrap().resourcepacks.clone() {
Some(folder) => folder, if self.packs.is_none() {
None => config.resourcepacks.clone() return Ok(())
} }
} else { for (k, v) in self.packs.clone().unwrap().iter() {
config.resourcepacks.clone() if v.release {
}; println!(" {} {} : {}", "╠════»".blue(), "Archiving Version".green(), k);
let zip_file = fs::File::create(format!("{}/{}.zip", resourcepacks, self.name))?; let zip_file = fs::File::create(format!("{}/{}-{}.zip", self.output_folder(config), self.name, k))?;
let mut zip_writer = ZipWriter::new(zip_file); let mut zip_writer = ZipWriter::new(zip_file);
let options = FileOptions::default() let options = FileOptions::default()
@@ -143,20 +193,75 @@ impl Pack {
for entry in WalkDir::new(path.clone()) for entry in WalkDir::new(path.clone())
.into_iter() .into_iter()
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
.filter(|e| {
if v.prefix == "" { // for the "global" instance of the pack / base pack
// ignoring all files that contain a . in the file stem as these are not global textures
!e.path().file_stem().expect("unable to get file stem").to_str().unwrap().contains(".")
} else {
e.path().file_stem().expect("unable to get file stem").to_str().unwrap().starts_with(format!("{}.", v.prefix).as_str())
}
})
.filter(|e| e.file_name().to_str().unwrap() != "pack.toml")
{ {
let file_path = entry.path(); let file_path = entry.path();
if file_path.is_file() { if file_path.is_file() {
let mut file = fs::File::open(file_path)?; let mut file = fs::File::open(file_path)?;
let relpath = file_path.strip_prefix(path.clone())?; let relpath = file_path.strip_prefix(path.clone())?;
let mut zip_path = PathBuf::new(); let mut zip_path = PathBuf::new();
if v.prefix == "" {
zip_path.push(relpath); zip_path.push(relpath);
} else {
let dir = relpath.parent().unwrap();
let filename = relpath
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_string()
.strip_prefix(format!("{}.", v.prefix).as_str())
.unwrap()
.to_string();
let final_path = dir.join(filename);
zip_path.push(final_path);
}
zip_writer.start_file(zip_path.to_string_lossy().into_owned(), options)?; zip_writer.start_file(zip_path.to_string_lossy().into_owned(), options)?;
std::io::copy(&mut file, &mut zip_writer)?; std::io::copy(&mut file, &mut zip_writer)?;
} }
} }
zip_writer.finish()?;
zip_writer.finish()?;
} else {
println!(" {} {} : {}", "╠════»".blue(), "Ignoring Version ".yellow(), k);
}
};
Ok(())
}
fn output_folder(&self, config: &Config) -> String {
if config.versions.contains_key(&self.version) {
match config.versions.get(&self.version).unwrap().resourcepacks.clone() {
Some(folder) => folder,
None => config.resourcepacks.clone()
}
} else {
config.resourcepacks.clone()
}
}
fn move_archive(&self, path: &PathBuf, config: &Config) -> io::Result<()> {
let span = span!(Level::INFO, "Moving Pack");
let _guard = span.enter();
if !fs::read_dir(self.output_folder(config)).unwrap().any(|f| f.unwrap().file_name().to_str().unwrap() == format!("{}.zip",self.name).as_str()) {
fs::copy(path, format!("{}/{}.zip", self.output_folder(config), self.name))?;
} else {
println!(" {} {}", "".blue(), " File Already Exists: Ignoring".yellow());
}
Ok(()) Ok(())
} }
} }
@@ -168,110 +273,6 @@ struct PackComponent {
release: bool, release: bool,
} }
//
// fn scan_directory(path: PathBuf, config: &Config) -> io::Result<()> {
// let mut pack_mcmeta = path.clone();
// pack_mcmeta.push("pack.mcmeta");
// if pack_mcmeta.exists() {
// // if pack metadata exists, put the pack into a zip folder.
// get_archive(path, config).map_err(|_| Error::new(ErrorKind::Other, "failed to archive zip"))?;
// return Ok(());
// } else {
// // recursively scans subdirectories for other packs
// for entry in fs::read_dir(path)? {
// let entry = entry?;
//
// if entry
// .path()
// .file_name()
// .unwrap()
// .to_os_string()
// .into_string()
// .map_err(|_| Error::new(ErrorKind::Other, "failed to convert os_str to string"))?
// .starts_with("_")
// {
// println!("found _");
// continue;
// }
// if let Some(zip_str) = entry.path().extension() {
// if zip_str == "zip" {
// fs::copy(
// entry.path(),
// format!(
// "{}/{}",
// config.resourcepacks,
// entry
// .path()
// .file_name()
// .unwrap()
// .to_os_string()
// .into_string()
// .map_err(|_| Error::new(
// ErrorKind::Other,
// "failed to convert os_str to string"
// ))?
// ),
// )?;
// }
// }
//
// if let Ok(file_type) = entry.file_type() {
// if file_type.is_dir() {
// scan_directory(entry.path(), &config)?;
// }
// }
// }
// }
// Ok(())
// }
//
// fn get_archive(path: PathBuf, config: &Config) -> Result<(), Box<dyn std::error::Error>> {
// println!("CREATING ARCHIVE: {}", &path.display());
// if let Some(f) = path.file_stem() {
// let mut filename = f
// .to_os_string()
// .into_string()
// .map_err(|_| Error::new(ErrorKind::Other, "failed to convert os_str to string"))?;
// filename.push_str("_0");
// loop {
// if !Path::new(&format!("{}/{}.zip", config.resourcepacks, filename.to_string())).exists() {
// // checks if zip file already exists
// let zip_file = fs::File::create(format!("{}/{}.zip", config.resourcepacks, filename))?;
// let mut zip_writer = ZipWriter::new(zip_file);
//
// let options = FileOptions::default()
// .compression_method(CompressionMethod::Deflated)
// .unix_permissions(0o755);
//
// for entry in WalkDir::new(path.clone())
// .into_iter()
// .filter_map(|e| e.ok())
// {
// let file_path = entry.path();
// if file_path.is_file() {
// let mut file = fs::File::open(file_path)?;
// let relpath = file_path.strip_prefix(path.clone())?;
// let mut zip_path = PathBuf::new();
// zip_path.push(relpath);
//
// zip_writer.start_file(zip_path.to_string_lossy().into_owned(), options)?;
// std::io::copy(&mut file, &mut zip_writer)?;
// }
// }
// zip_writer.finish()?;
// break;
// } else {
// // 10 increments the last digit of the filename by 1
// // TODO: add support for more than one digit
// let i: u32 = filename.pop().unwrap().to_digit(10).unwrap();
// filename.push(char::from_digit(i + 1, 10).unwrap());
// }
// }
// }
// Ok(())
// }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct VersionConfig { pub struct VersionConfig {
pub pack_repo: Option<String>, pub pack_repo: Option<String>,
@@ -308,16 +309,10 @@ impl Config {
if conf_path.exists() { if conf_path.exists() {
let config: Config = toml::from_str(fs::read_to_string(&conf_path)?.as_str()) let config: Config = toml::from_str(fs::read_to_string(&conf_path)?.as_str())
.map_err(|_| Error::new(ErrorKind::Other, "failed to deserialise"))?; .map_err(|_| Error::new(ErrorKind::Other, "failed to deserialise"))?;
info!( info!("Loaded Config File: \n\trepo: {}\n\tpacks folder: {}\n\tignore with prefix: {}", config.repository, config.resourcepacks, config.ignore_folder_prefix);
"Loaded Config File: \n\trepo: {}\n\tpacks folder: {}\n\tignore with prefix: {}",
config.repository, config.resourcepacks, config.ignore_folder_prefix
);
Ok(config) Ok(config)
} else { } else {
warn!( warn!("No config file detected. Creating config file with default values at:\n\t{}", conf_path.display());
"No config file detected. Creating config file with default values at:\n\t{}",
conf_path.display()
);
create_config_file(&conf_path)?; create_config_file(&conf_path)?;
Ok(Config::default()) Ok(Config::default())
} }
@@ -326,21 +321,21 @@ impl Config {
fn path() -> Option<PathBuf> { fn path() -> Option<PathBuf> {
Some(PathBuf::from(".").join("config.toml")) Some(PathBuf::from(".").join("config.toml"))
} }
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
fn path() -> Option<PathBuf> { fn path() -> Option<PathBuf> {
dirs::config_dir()?.join("pack_sync").join("config.toml") Some(dirs::config_dir()?.join("pack_sync").join("config.toml"))
} }
} }
fn create_config_file(path: &PathBuf) -> Result<(), Error> { fn create_config_file(path: &PathBuf) -> Result<(), Error> {
println!("{}", path.display()); println!("{}", path.display());
fs::create_dir_all(path.parent().unwrap())?; fs::create_dir_all(path.parent().unwrap())?;
fs::File::create(path)?; fs::File::create(path)?;
let yaml = serde_yaml::to_string(&Config::default()) let output = toml::to_string(&Config::default())
.map_err(|_| Error::new(ErrorKind::Other, "failed to serialise"))?; .map_err(|_| Error::new(ErrorKind::Other, "failed to serialise"))?;
let mut file = OpenOptions::new() let mut file = OpenOptions::new()
@@ -348,7 +343,7 @@ fn create_config_file(path: &PathBuf) -> Result<(), Error> {
.create(true) .create(true)
.truncate(true) .truncate(true)
.open(path)? .open(path)?
.write_all(yaml.as_bytes())?; .write_all(output.as_bytes())?;
Ok(()) Ok(())
} }