1use std::{
2 io::{self, BufReader, Write as _},
3 mem::forget,
4 path::{Path, PathBuf},
5 time::Instant,
6};
7
8use anyhow::Context;
9use clap::Parser;
10use fs_err::File;
11use memofs::Vfs;
12use rbx_dom_weak::{InstanceBuilder, WeakDom};
13use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
14
15use crate::{
16 path_serializer::display_absolute,
17 serve_session::ServeSession,
18 syncback::{syncback_loop, FsSnapshot},
19};
20
21use super::{resolve_path, GlobalOptions};
22
23const UNKNOWN_INPUT_KIND_ERR: &str = "Could not detect what kind of file was inputted. \
24 Expected input file to end in .rbxl, .rbxlx, .rbxm, or .rbxmx.";
25
26#[derive(Debug, Parser)]
35pub struct SyncbackCommand {
36 #[clap(default_value = "")]
38 pub project: PathBuf,
39
40 #[clap(long, short)]
42 pub input: PathBuf,
43
44 #[clap(long, short)]
47 pub list: bool,
48
49 #[clap(long)]
52 pub dry_run: bool,
53
54 #[clap(long, short = 'y')]
56 pub non_interactive: bool,
57}
58
59impl SyncbackCommand {
60 pub fn run(&self, global: GlobalOptions) -> anyhow::Result<()> {
61 let path_old = resolve_path(&self.project)?;
62 let path_new = resolve_path(&self.input)?;
63
64 let input_kind = FileKind::from_path(&path_new).context(UNKNOWN_INPUT_KIND_ERR)?;
65 let dom_start_timer = Instant::now();
66 let dom_new = read_dom(&path_new, input_kind)?;
67 log::debug!(
68 "Finished opening file in {:0.02}s",
69 dom_start_timer.elapsed().as_secs_f32()
70 );
71
72 let vfs = Vfs::new_default()?;
73 vfs.set_watch_enabled(false);
74
75 let project_start_timer = Instant::now();
76 let session_old = ServeSession::new(vfs, path_old.clone())?;
77 log::debug!(
78 "Finished opening project in {:0.02}s",
79 project_start_timer.elapsed().as_secs_f32()
80 );
81
82 let mut dom_old = session_old.tree();
83
84 log::debug!("Old root: {}", dom_old.inner().root().class);
85 log::debug!("New root: {}", dom_new.root().class);
86
87 if log::log_enabled!(log::Level::Trace) {
88 log::trace!("Children of old root:");
89 for child in dom_old.inner().root().children() {
90 let inst = dom_old.get_instance(*child).unwrap();
91 log::trace!("{} (class: {})", inst.name(), inst.class_name());
92 }
93 log::trace!("Children of new root:");
94 for child in dom_new.root().children() {
95 let inst = dom_new.get_by_ref(*child).unwrap();
96 log::trace!("{} (class: {})", inst.name, inst.class);
97 }
98 }
99
100 let syncback_timer = Instant::now();
101 eprintln!("Beginning syncback...");
102 let snapshot = syncback_loop(
103 session_old.vfs(),
104 &mut dom_old,
105 dom_new,
106 session_old.root_project(),
107 )?;
108 log::debug!(
109 "Syncback finished in {:.02}s!",
110 syncback_timer.elapsed().as_secs_f32()
111 );
112
113 let base_path = session_old.root_project().folder_location();
114 if self.list {
115 list_files(&snapshot, global.color.into(), base_path)?;
116 }
117
118 if !self.dry_run {
119 if !self.non_interactive {
120 eprintln!(
121 "Would write {} files/folders and remove {} files/folders.",
122 snapshot.added_paths().len(),
123 snapshot.removed_paths().len()
124 );
125 eprint!("Is this okay? (Y/N): ");
126 io::stderr().flush()?;
127 let mut line = String::with_capacity(1);
128 io::stdin().read_line(&mut line)?;
129 line = line.trim().to_lowercase();
130 if line != "y" {
131 eprintln!("Aborting due to user input!");
132 return Ok(());
133 }
134 }
135 eprintln!("Writing to the file system...");
136 snapshot.write_to_vfs(base_path, session_old.vfs())?;
137 eprintln!("Finished syncback.")
138 } else {
139 eprintln!(
140 "Would write {} files/folders and remove {} files/folders.",
141 snapshot.added_paths().len(),
142 snapshot.removed_paths().len()
143 );
144 eprintln!("Aborting before writing to file system due to `--dry-run`");
145 }
146
147 drop(dom_old);
151 forget(session_old);
152
153 Ok(())
154 }
155}
156
157fn read_dom(path: &Path, file_kind: FileKind) -> anyhow::Result<WeakDom> {
158 let content = BufReader::new(File::open(path)?);
159 match file_kind {
160 FileKind::Rbxl => rbx_binary::from_reader(content).with_context(|| {
161 format!(
162 "Could not deserialize binary place file at {}",
163 path.display()
164 )
165 }),
166 FileKind::Rbxlx => rbx_xml::from_reader(content, xml_decode_config())
167 .with_context(|| format!("Could not deserialize XML place file at {}", path.display())),
168 FileKind::Rbxm => {
169 let temp_tree = rbx_binary::from_reader(content).with_context(|| {
170 format!(
171 "Could not deserialize binary place file at {}",
172 path.display()
173 )
174 })?;
175
176 process_model_dom(temp_tree)
177 }
178 FileKind::Rbxmx => {
179 let temp_tree =
180 rbx_xml::from_reader(content, xml_decode_config()).with_context(|| {
181 format!("Could not deserialize XML model file at {}", path.display())
182 })?;
183 process_model_dom(temp_tree)
184 }
185 }
186}
187
188fn process_model_dom(dom: WeakDom) -> anyhow::Result<WeakDom> {
189 let temp_children = dom.root().children();
190 if temp_children.len() == 1 {
191 let real_root = dom.get_by_ref(temp_children[0]).unwrap();
192 let mut new_tree = WeakDom::new(InstanceBuilder::new(real_root.class));
193 for (name, property) in &real_root.properties {
194 new_tree
195 .root_mut()
196 .properties
197 .insert(*name, property.to_owned());
198 }
199
200 let children = dom.clone_multiple_into_external(real_root.children(), &mut new_tree);
201 for child in children {
202 new_tree.transfer_within(child, new_tree.root_ref());
203 }
204 Ok(new_tree)
205 } else {
206 anyhow::bail!(
207 "Rojo does not currently support models with more \
208 than one Instance at the Root!"
209 );
210 }
211}
212
213fn xml_decode_config() -> rbx_xml::DecodeOptions<'static> {
214 rbx_xml::DecodeOptions::new().property_behavior(rbx_xml::DecodePropertyBehavior::ReadUnknown)
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219enum FileKind {
220 Rbxmx,
222
223 Rbxlx,
225
226 Rbxm,
228
229 Rbxl,
231}
232
233impl FileKind {
234 fn from_path(output: &Path) -> Option<FileKind> {
235 let extension = output.extension()?.to_str()?;
236
237 match extension {
238 "rbxlx" => Some(FileKind::Rbxlx),
239 "rbxmx" => Some(FileKind::Rbxmx),
240 "rbxl" => Some(FileKind::Rbxl),
241 "rbxm" => Some(FileKind::Rbxm),
242 _ => None,
243 }
244 }
245}
246
247fn list_files(snapshot: &FsSnapshot, color: ColorChoice, base_path: &Path) -> io::Result<()> {
248 let no_color = ColorSpec::new();
249 let mut add_color = ColorSpec::new();
250 add_color.set_fg(Some(Color::Green));
251 let mut remove_color = ColorSpec::new();
252 remove_color.set_fg(Some(Color::Red));
253
254 let writer = BufferWriter::stdout(color);
255 let mut buffer = writer.buffer();
256
257 let added = snapshot.added_paths();
258 if !added.is_empty() {
259 buffer.set_color(&add_color)?;
260 for path in added {
261 writeln!(
262 &mut buffer,
263 "Writing {}",
264 display_absolute(path.strip_prefix(base_path).unwrap_or(path))
265 )?;
266 }
267 }
268 let removed = snapshot.removed_paths();
269 if !removed.is_empty() {
270 buffer.set_color(&remove_color)?;
271 for path in removed {
272 writeln!(
273 &mut buffer,
274 "Removing {}",
275 display_absolute(path.strip_prefix(base_path).unwrap_or(path))
276 )?;
277 }
278 }
279 buffer.set_color(&no_color)?;
280
281 writer.print(&buffer)
282}