1#![allow(dead_code)]
7
8mod csv;
9mod dir;
10mod json;
11mod json_model;
12mod lua;
13mod meta_file;
14mod project;
15mod rbxm;
16mod rbxmx;
17mod toml;
18mod txt;
19mod util;
20mod yaml;
21
22use std::{
23 path::{Path, PathBuf},
24 sync::OnceLock,
25};
26
27use anyhow::Context;
28use memofs::{IoResultExt, Vfs};
29use serde::{Deserialize, Serialize};
30
31use crate::{
32 glob::Glob,
33 project::DEFAULT_PROJECT_NAMES,
34 syncback::{SyncbackReturn, SyncbackSnapshot},
35};
36use crate::{
37 snapshot::{InstanceContext, InstanceSnapshot, SyncRule},
38 syncback::validate_file_name,
39};
40
41use self::{
42 csv::{snapshot_csv, snapshot_csv_init, syncback_csv, syncback_csv_init},
43 dir::{snapshot_dir, syncback_dir},
44 json::snapshot_json,
45 json_model::{snapshot_json_model, syncback_json_model},
46 lua::{snapshot_lua, snapshot_lua_init, syncback_lua, syncback_lua_init},
47 project::{snapshot_project, syncback_project},
48 rbxm::{snapshot_rbxm, syncback_rbxm},
49 rbxmx::{snapshot_rbxmx, syncback_rbxmx},
50 toml::snapshot_toml,
51 txt::{snapshot_txt, syncback_txt},
52 yaml::snapshot_yaml,
53};
54
55pub use self::{
56 lua::ScriptType, project::snapshot_project_node, util::emit_legacy_scripts_default,
57 util::PathExt,
58};
59
60#[profiling::function]
65pub fn snapshot_from_vfs(
66 context: &InstanceContext,
67 vfs: &Vfs,
68 path: &Path,
69) -> anyhow::Result<Option<InstanceSnapshot>> {
70 let meta = match vfs.metadata(path).with_not_found()? {
71 Some(meta) => meta,
72 None => return Ok(None),
73 };
74
75 if meta.is_dir() {
76 let (middleware, dir_name, init_path) = get_dir_middleware(vfs, path)?;
77 match middleware {
82 Middleware::Dir => middleware.snapshot(context, vfs, path, dir_name),
83 _ => middleware.snapshot(context, vfs, &init_path, dir_name),
84 }
85 } else {
86 let file_name = path
87 .file_name()
88 .and_then(|n| n.to_str())
89 .with_context(|| format!("file name of {} is invalid", path.display()))?;
90
91 match file_name {
93 "init.server.luau" | "init.server.lua" | "init.client.luau" | "init.client.lua"
94 | "init.plugin.luau" | "init.plugin.lua" | "init.luau" | "init.lua" | "init.csv" => {
95 return Ok(None)
96 }
97 _ => {}
98 }
99
100 snapshot_from_path(context, vfs, path)
101 }
102}
103
104fn get_dir_middleware<'path>(
111 vfs: &Vfs,
112 dir_path: &'path Path,
113) -> anyhow::Result<(Middleware, &'path str, PathBuf)> {
114 let dir_name = dir_path
115 .file_name()
116 .expect("Could not extract directory name")
117 .to_str()
118 .ok_or_else(|| anyhow::anyhow!("File name was not valid UTF-8: {}", dir_path.display()))?;
119
120 static INIT_PATHS: OnceLock<Vec<(Middleware, &str)>> = OnceLock::new();
121 let order = INIT_PATHS.get_or_init(|| {
122 vec![
123 (Middleware::ModuleScriptDir, "init.luau"),
124 (Middleware::ModuleScriptDir, "init.lua"),
125 (Middleware::ServerScriptDir, "init.server.luau"),
126 (Middleware::ServerScriptDir, "init.server.lua"),
127 (Middleware::ClientScriptDir, "init.client.luau"),
128 (Middleware::ClientScriptDir, "init.client.lua"),
129 (Middleware::PluginScriptDir, "init.plugin.lua"),
130 (Middleware::PluginScriptDir, "init.plugin.luau"),
131 (Middleware::CsvDir, "init.csv"),
132 ]
133 });
134
135 for default_project_name in DEFAULT_PROJECT_NAMES {
136 let project_path = dir_path.join(default_project_name);
137 if vfs.metadata(&project_path).with_not_found()?.is_some() {
138 return Ok((Middleware::Project, dir_name, project_path));
139 }
140 }
141
142 for (middleware, name) in order {
143 let test_path = dir_path.join(name);
144 if vfs.metadata(&test_path).with_not_found()?.is_some() {
145 return Ok((*middleware, dir_name, test_path));
146 }
147 }
148
149 Ok((Middleware::Dir, dir_name, dir_path.to_path_buf()))
150}
151
152fn snapshot_from_path(
155 context: &InstanceContext,
156 vfs: &Vfs,
157 path: &Path,
158) -> anyhow::Result<Option<InstanceSnapshot>> {
159 if let Some(rule) = context.get_user_sync_rule(path) {
160 return rule
161 .middleware
162 .snapshot(context, vfs, path, rule.file_name_for_path(path)?);
163 } else {
164 for rule in default_sync_rules() {
165 if rule.matches(path) {
166 return rule.middleware.snapshot(
167 context,
168 vfs,
169 path,
170 rule.file_name_for_path(path)?,
171 );
172 }
173 }
174 }
175 Ok(None)
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
184#[serde(rename_all = "camelCase")]
185pub enum Middleware {
186 Csv,
187 JsonModel,
188 Json,
189 ServerScript,
190 ClientScript,
191 ModuleScript,
192 PluginScript,
193 LegacyClientScript,
194 LegacyServerScript,
195 RunContextServerScript,
196 RunContextClientScript,
197 Project,
198 Rbxm,
199 Rbxmx,
200 Toml,
201 Text,
202 Yaml,
203 Ignore,
204
205 #[serde(skip_deserializing)]
206 Dir,
207 #[serde(skip_deserializing)]
208 ServerScriptDir,
209 #[serde(skip_deserializing)]
210 ClientScriptDir,
211 #[serde(skip_deserializing)]
212 PluginScriptDir,
213 #[serde(skip_deserializing)]
214 ModuleScriptDir,
215 #[serde(skip_deserializing)]
216 CsvDir,
217}
218
219impl Middleware {
220 fn snapshot(
223 &self,
224 context: &InstanceContext,
225 vfs: &Vfs,
226 path: &Path,
227 name: &str,
228 ) -> anyhow::Result<Option<InstanceSnapshot>> {
229 let mut output = match self {
230 Self::Csv => snapshot_csv(context, vfs, path, name),
231 Self::JsonModel => snapshot_json_model(context, vfs, path, name),
232 Self::Json => snapshot_json(context, vfs, path, name),
233 Self::ServerScript => snapshot_lua(context, vfs, path, name, ScriptType::Server),
234 Self::ClientScript => snapshot_lua(context, vfs, path, name, ScriptType::Client),
235 Self::ModuleScript => snapshot_lua(context, vfs, path, name, ScriptType::Module),
236 Self::PluginScript => snapshot_lua(context, vfs, path, name, ScriptType::Plugin),
237 Self::LegacyClientScript => {
238 snapshot_lua(context, vfs, path, name, ScriptType::LegacyClient)
239 }
240 Self::LegacyServerScript => {
241 snapshot_lua(context, vfs, path, name, ScriptType::LegacyServer)
242 }
243 Self::RunContextClientScript => {
244 snapshot_lua(context, vfs, path, name, ScriptType::RunContextClient)
245 }
246 Self::RunContextServerScript => {
247 snapshot_lua(context, vfs, path, name, ScriptType::RunContextServer)
248 }
249 Self::Project => snapshot_project(context, vfs, path, name),
250 Self::Rbxm => snapshot_rbxm(context, vfs, path, name),
251 Self::Rbxmx => snapshot_rbxmx(context, vfs, path, name),
252 Self::Toml => snapshot_toml(context, vfs, path, name),
253 Self::Text => snapshot_txt(context, vfs, path, name),
254 Self::Yaml => snapshot_yaml(context, vfs, path, name),
255 Self::Ignore => Ok(None),
256
257 Self::Dir => snapshot_dir(context, vfs, path, name),
258 Self::ServerScriptDir => {
259 snapshot_lua_init(context, vfs, path, name, ScriptType::Server)
260 }
261 Self::ClientScriptDir => {
262 snapshot_lua_init(context, vfs, path, name, ScriptType::Client)
263 }
264 Self::PluginScriptDir => {
265 snapshot_lua_init(context, vfs, path, name, ScriptType::Plugin)
266 }
267 Self::ModuleScriptDir => {
268 snapshot_lua_init(context, vfs, path, name, ScriptType::Module)
269 }
270 Self::CsvDir => snapshot_csv_init(context, vfs, path, name),
271 };
272 if let Ok(Some(ref mut snapshot)) = output {
273 snapshot.metadata.middleware = Some(*self);
274 }
275 output
276 }
277
278 pub fn syncback<'sync>(
281 &self,
282 snapshot: &SyncbackSnapshot<'sync>,
283 ) -> anyhow::Result<SyncbackReturn<'sync>> {
284 let file_name = snapshot.path.file_name().and_then(|s| s.to_str());
285 if let Some(file_name) = file_name {
286 validate_file_name(file_name).with_context(|| {
287 format!("cannot create a file or directory with name {file_name}")
288 })?;
289 }
290 match self {
291 Middleware::Csv => syncback_csv(snapshot),
292 Middleware::JsonModel => syncback_json_model(snapshot),
293 Middleware::Json => anyhow::bail!("cannot syncback Json middleware"),
294 Middleware::Project => syncback_project(snapshot),
297 Middleware::ServerScript => syncback_lua(snapshot),
298 Middleware::ClientScript => syncback_lua(snapshot),
299 Middleware::ModuleScript => syncback_lua(snapshot),
300 Middleware::Rbxm => syncback_rbxm(snapshot),
301 Middleware::Rbxmx => syncback_rbxmx(snapshot),
302 Middleware::Toml => anyhow::bail!("cannot syncback Toml middleware"),
303 Middleware::Text => syncback_txt(snapshot),
304 Middleware::Yaml => anyhow::bail!("cannot syncback Yaml middleware"),
305 Middleware::Ignore => anyhow::bail!("cannot syncback Ignore middleware"),
306 Middleware::Dir => syncback_dir(snapshot),
307 Middleware::ServerScriptDir => syncback_lua_init(ScriptType::Server, snapshot),
308 Middleware::ClientScriptDir => syncback_lua_init(ScriptType::Client, snapshot),
309 Middleware::PluginScriptDir => syncback_lua_init(ScriptType::Plugin, snapshot),
310 Middleware::ModuleScriptDir => syncback_lua_init(ScriptType::Module, snapshot),
311 Middleware::CsvDir => syncback_csv_init(snapshot),
312
313 Middleware::PluginScript
314 | Middleware::LegacyServerScript
315 | Middleware::LegacyClientScript
316 | Middleware::RunContextServerScript
317 | Middleware::RunContextClientScript => {
318 anyhow::bail!("syncback is not implemented for {self:?} yet")
319 }
320 }
321 }
322
323 #[inline]
325 pub fn is_dir(&self) -> bool {
326 matches!(
327 self,
328 Middleware::Dir
329 | Middleware::ServerScriptDir
330 | Middleware::ClientScriptDir
331 | Middleware::PluginScriptDir
332 | Middleware::ModuleScriptDir
333 | Middleware::CsvDir
334 )
335 }
336
337 #[inline]
345 pub fn handles_own_properties(&self) -> bool {
346 matches!(
347 self,
348 Middleware::JsonModel | Middleware::Project | Middleware::Rbxm | Middleware::Rbxmx
349 )
350 }
351
352 pub fn middleware_for_path(
356 vfs: &Vfs,
357 sync_rules: &[SyncRule],
358 path: &Path,
359 ) -> anyhow::Result<Option<Self>> {
360 let meta = match vfs.metadata(path).with_not_found()? {
361 Some(meta) => meta,
362 None => return Ok(None),
363 };
364
365 if meta.is_dir() {
366 let (middleware, _, _) = get_dir_middleware(vfs, path)?;
367 Ok(Some(middleware))
368 } else {
369 for rule in sync_rules.iter().chain(default_sync_rules()) {
370 if rule.matches(path) {
371 return Ok(Some(rule.middleware));
372 }
373 }
374 Ok(None)
375 }
376 }
377}
378
379macro_rules! sync_rule {
387 ($pattern:expr, $middleware:ident) => {
388 SyncRule {
389 middleware: Middleware::$middleware,
390 include: Glob::new($pattern).unwrap(),
391 exclude: None,
392 suffix: None,
393 base_path: PathBuf::new(),
394 }
395 };
396 ($pattern:expr, $middleware:ident, $suffix:expr) => {
397 SyncRule {
398 middleware: Middleware::$middleware,
399 include: Glob::new($pattern).unwrap(),
400 exclude: None,
401 suffix: Some($suffix.into()),
402 base_path: PathBuf::new(),
403 }
404 };
405 ($pattern:expr, $middleware:ident, $suffix:expr, $exclude:expr) => {
406 SyncRule {
407 middleware: Middleware::$middleware,
408 include: Glob::new($pattern).unwrap(),
409 exclude: Some(Glob::new($exclude).unwrap()),
410 suffix: Some($suffix.into()),
411 base_path: PathBuf::new(),
412 }
413 };
414}
415
416pub fn default_sync_rules() -> &'static [SyncRule] {
420 static DEFAULT_SYNC_RULES: OnceLock<Vec<SyncRule>> = OnceLock::new();
421
422 DEFAULT_SYNC_RULES.get_or_init(|| {
423 vec![
424 sync_rule!("*.server.lua", ServerScript, ".server.lua"),
425 sync_rule!("*.server.luau", ServerScript, ".server.luau"),
426 sync_rule!("*.client.lua", ClientScript, ".client.lua"),
427 sync_rule!("*.client.luau", ClientScript, ".client.luau"),
428 sync_rule!("*.plugin.lua", PluginScript, ".plugin.lua"),
429 sync_rule!("*.plugin.luau", PluginScript, ".plugin.luau"),
430 sync_rule!("*.{lua,luau}", ModuleScript),
431 sync_rule!("*.project.json", Project, ".project.json"),
432 sync_rule!("*.project.jsonc", Project, ".project.jsonc"),
433 sync_rule!("*.model.json", JsonModel, ".model.json"),
434 sync_rule!("*.model.jsonc", JsonModel, ".model.jsonc"),
435 sync_rule!("*.json", Json, ".json", "*.meta.json"),
436 sync_rule!("*.jsonc", Json, ".jsonc", "*.meta.jsonc"),
437 sync_rule!("*.toml", Toml),
438 sync_rule!("*.csv", Csv),
439 sync_rule!("*.txt", Text),
440 sync_rule!("*.rbxmx", Rbxmx),
441 sync_rule!("*.rbxm", Rbxm),
442 sync_rule!("*.{yml,yaml}", Yaml),
443 ]
444 })
445}