1mod archival_error;
2#[cfg(test)]
3mod build_id_tests;
4mod definition_comments;
5mod file_system;
6mod file_system_memory;
7mod file_system_mutex;
8#[cfg(test)]
9mod file_system_tests;
10mod filters;
11mod lib_fs;
12mod liquid_parser;
13mod liquid_rewrite;
14#[cfg(feature = "lsp")]
15mod lsp;
16mod object_definition;
17mod page;
18mod read_toml;
19mod reserved_fields;
20#[cfg(test)]
21mod schema_files;
22pub mod schemas;
23mod site;
24mod tags;
25#[cfg(test)]
26mod test_utils;
27mod typescript_defs;
28mod util;
29mod value_path;
30use anyhow::Result;
31use events::{
32 AddChildEvent, AddObjectEvent, ArchivalEvent, DeleteObjectEvent, EditFieldEvent,
33 EditOrderEvent, RemoveChildEvent, RenameObjectEvent,
34};
35use events::{AddRootObjectEvent, ArchivalEventResponse};
36use manifest::Manifest;
37use mime_guess::MimeGuess;
38use seahash::SeaHasher;
39use serde::{Deserialize, Serialize};
40use sha2::{Digest, Sha256};
41use site::Site;
42use std::cmp::Ordering;
43use std::fmt::Debug;
44use std::hash::Hasher;
45use std::path::{Path, PathBuf};
46use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
47use tracing::{debug, error};
48#[cfg(feature = "binary")]
49pub mod binary;
50mod constants;
51#[cfg(feature = "stdlib-fs")]
52mod file_system_stdlib;
53#[cfg(feature = "json-schema")]
54mod json_schema;
55#[cfg(feature = "binary")]
56mod server;
57use file_system_mutex::FileSystemMutex;
58use object::{Object, ObjectEntry};
59use semver::{Version, VersionReq};
60
61pub mod events;
63pub mod fields;
64pub mod manifest;
65pub mod object;
66#[cfg(feature = "proto")]
67pub mod proto;
68pub use archival_error::ArchivalError;
69pub use constants::{
70 LEGACY_MANIFEST_FILE_NAME, LEGACY_OBJECT_DEFINITION_FILE_NAME, MANIFEST_FILE_NAME,
71 MIN_COMPAT_VERSION, OBJECT_DEFINITION_FILE_NAME,
72};
73pub use definition_comments::DefinitionComments;
74pub use fields::{
75 file::RenderedFile, FieldConfig, FieldType, FieldValue, RenderedFieldValue,
76 RenderedObjectValues,
77};
78pub use file_system::{unpack_zip, FileSystemAPI};
79pub use file_system_memory::MemoryFileSystem;
80#[cfg(feature = "json-schema")]
81pub use json_schema::{ObjectSchema, ObjectSchemaOptions};
82pub use object::{ObjectMap, RenderedObject, RenderedObjectMap, ValuePath};
83pub use object_definition::{FieldDefinition, FieldsMap, ObjectDefinition, ObjectDefinitions};
84#[cfg(feature = "proto")]
85pub use proto::archival_proto;
86pub use typescript_defs::generate_typescript_defs;
87
88pub type ArchivalBuildId = u64;
89
90#[cfg(feature = "typescript")]
91pub mod typedefs {
92 pub use crate::object::typedefs::*;
93 pub use crate::object_definition::typedefs::*;
94}
95
96#[derive(Debug, Default)]
97pub struct BuildOptions {
98 pub skip_static: bool,
99 pub skip_failures: bool,
100}
101
102impl BuildOptions {
103 pub fn no_static() -> Self {
104 Self {
105 skip_static: true,
106 skip_failures: false,
107 }
108 }
109 pub fn intermediate() -> Self {
110 Self {
111 skip_static: true,
112 skip_failures: true,
113 }
114 }
115}
116
117#[derive(Debug, Serialize, Deserialize)]
118pub struct DistFile {
119 pub path: PathBuf,
120 pub mime: String,
121 pub data: Vec<u8>,
122}
123impl DistFile {
124 fn new(path: PathBuf, data: Vec<u8>) -> Self {
125 Self {
126 mime: MimeGuess::from_path(&path)
127 .first_or_octet_stream()
128 .essence_str()
129 .to_string(),
130 data,
131 path,
132 }
133 }
134}
135
136pub static ARCHIVAL_VERSION: &str = env!("CARGO_PKG_VERSION");
137
138pub(crate) fn check_compatibility(version_string: &str) -> (bool, String) {
139 let req = VersionReq::parse(MIN_COMPAT_VERSION).unwrap();
140 match Version::parse(version_string) {
141 Ok(version) => {
142 if req.matches(&version) {
143 (true, "passed compatibility check.".to_owned())
144 } else {
145 (false, format!("site archival version {} is incompatible with this version of archival (minimum required version {}).", version, MIN_COMPAT_VERSION))
146 }
147 }
148 Err(e) => (false, format!("invalid version {}: {}", version_string, e)),
149 }
150}
151
152pub fn sha_for_data(data: &[u8]) -> String {
153 let mut hasher = Sha256::new();
154 hasher.update(data);
155 data_encoding::HEXLOWER.encode(&hasher.finalize())
156}
157
158#[derive(Debug)]
159pub struct Archival<F: FileSystemAPI + Clone + Debug> {
160 fs_mutex: FileSystemMutex<F>,
161 pub site: site::Site,
162 last_build_id: AtomicU64,
163}
164
165impl<F: FileSystemAPI + Clone + Debug> Archival<F> {
166 pub fn is_compatible(fs: &F) -> Result<bool> {
167 let site = Site::load(fs, Some(""))?;
168 if let Some(version_str) = &site.manifest.archival_version {
169 let (ok, msg) = check_compatibility(version_str);
170 if !ok {
171 error!("incompatible: {}", msg);
172 }
173 Ok(ok)
174 } else {
175 Ok(true)
176 }
177 }
178 pub fn new(fs: F) -> Result<Self> {
179 let site = Site::load(&fs, None)?;
180 let fs_mutex = FileSystemMutex::init(fs);
181 Ok(Self {
182 fs_mutex,
183 site,
184 last_build_id: AtomicU64::new(0),
185 })
186 }
187 pub fn new_with_upload_prefix(fs: F, upload_prefix: &str) -> Result<Self> {
188 let site = Site::load(&fs, Some(upload_prefix))?;
189 let fs_mutex = FileSystemMutex::init(fs);
190 Ok(Self {
191 fs_mutex,
192 site,
193 last_build_id: AtomicU64::new(0),
194 })
195 }
196 pub fn build(&self, options: BuildOptions) -> Result<ArchivalBuildId> {
197 let (build_id, built) = self.fs_mutex.with_fs(|fs| {
198 if !options.skip_static {
199 self.site.sync_static_files(fs)?;
200 }
201 let build_id = self.site.build_id();
202 let should_build =
203 build_id == 0 || self.last_build_id.load(AtomicOrdering::Relaxed) != build_id;
204 if should_build {
205 debug!("build {} {:#?}", self.site, options);
206 self.site.build(fs, options)?;
207 } else {
208 #[cfg(feature = "verbose-logging")]
209 debug!("skipping duplicate build");
210 }
211 let final_build_id = self.site.build_id();
213 Ok((final_build_id, should_build))
214 })?;
215 if built {
217 self.last_build_id
218 .fetch_update(AtomicOrdering::Relaxed, AtomicOrdering::Relaxed, |_| {
219 Some(build_id)
220 })
221 .unwrap();
222 }
223 Ok(self.last_build_id.load(AtomicOrdering::Relaxed))
224 }
225 #[cfg(feature = "json-schema")]
226 pub fn dump_schemas(&self) -> Result<()> {
227 debug!("dump schemas {}", self.site);
228 self.fs_mutex.with_fs(|fs| self.site.dump_schemas(fs))
229 }
230 #[cfg(feature = "json-schema")]
231 pub fn generate_root_json_schema(&self, options: ObjectSchemaOptions) -> ObjectSchema {
232 json_schema::generate_root_json_schema(
233 &format!("{}/root.schema.json", self.site.schema_prefix()),
234 self.site.manifest.site_name.as_deref(),
235 &format!(
236 "Object definitions{}",
237 if let Some(name) = options
238 .name
239 .as_ref()
240 .and(self.site.manifest.site_name.as_ref())
241 .to_owned()
242 {
243 format!(" for {}", name)
244 } else {
245 "".to_string()
246 }
247 ),
248 &self.site.object_definitions,
249 &self
250 .fs_mutex
251 .with_fs(|fs| Ok(self.site.root_objects(fs)))
252 .unwrap(),
253 options,
254 )
255 }
256 pub fn dist_file(&self, path: &Path) -> Option<Vec<u8>> {
257 let path = self.site.manifest.build_dir.join(path);
258 self.fs_mutex.with_fs(|fs| fs.read(&path)).unwrap_or(None)
259 }
260 pub fn dist_files(&self) -> Vec<DistFile> {
261 let mut files = vec![];
262 self.fs_mutex
263 .with_fs(|fs| {
264 let build_dir = &self.site.manifest.build_dir;
265 for file in fs.walk_dir(build_dir, true)? {
266 if let Some(data) = fs.read(build_dir.join(&file)).unwrap_or(None) {
267 files.push(DistFile::new(file, data));
268 }
269 }
270 Ok(())
271 })
272 .unwrap();
273 files
274 }
275 pub fn object_exists(&self, obj_type: &str, filename: &str) -> Result<bool> {
276 self.fs_mutex
277 .with_fs(|fs| fs.exists(&self.object_path_impl(obj_type, filename, fs)?))
278 }
279 pub fn object_path(&self, obj_type: &str, filename: &str) -> PathBuf {
280 self.fs_mutex
281 .with_fs(|fs| Ok(self.object_path_impl(obj_type, filename, fs).unwrap()))
282 .unwrap()
283 }
284 pub fn build_id(&self) -> u64 {
285 self.site.build_id()
286 }
287 pub fn objects_generation(&self) -> u64 {
289 self.site.objects_generation()
290 }
291 pub fn fs_id(&self) -> Result<u64> {
292 self.fs_mutex.with_fs(|fs| self.fs_id_for_fs(fs))
293 }
294 pub fn list_build_files(&self) -> Result<impl Iterator<Item = PathBuf> + use<'_, F>> {
295 self.fs_mutex.with_fs(|fs| self.list_build_files_for_fs(fs))
296 }
297 fn list_build_files_for_fs(
298 &self,
299 fs: &F,
300 ) -> Result<impl Iterator<Item = PathBuf> + use<'_, F>> {
301 let Manifest {
302 object_definition_file,
303 pages_dir,
304 layout_dir,
305 objects_dir,
306 static_dir,
307 ..
308 } = &self.site.manifest;
309 let root_files = [
310 Manifest::path_in(Path::new(""), fs)?,
311 object_definition_file.to_owned(),
312 ];
313 Ok(root_files
314 .into_iter()
315 .chain(fs.walk_dir(static_dir, false)?.map(|p| static_dir.join(p)))
316 .chain(fs.walk_dir(pages_dir, false)?.map(|p| pages_dir.join(p)))
317 .chain(fs.walk_dir(layout_dir, false)?.map(|p| layout_dir.join(p)))
318 .chain(
319 fs.walk_dir(objects_dir, false)?
320 .map(|p| objects_dir.join(p)),
321 ))
322 }
323 fn fs_id_for_fs(&self, fs: &F) -> Result<u64> {
324 let mut hasher = SeaHasher::new();
325 for path in self.list_build_files_for_fs(fs)? {
326 if let Some(file) = fs.read(&path)? {
327 hasher.write(&file);
328 } else {
329 debug!("no content found for {}", path.display());
330 }
331 }
332 Ok(hasher.finish())
333 }
334 fn object_path_impl(&self, obj_type: &str, filename: &str, fs: &F) -> Result<PathBuf> {
335 let objects = self.site.get_objects(fs)?;
336 let entry = objects.get(obj_type).ok_or(ArchivalError::new(&format!(
337 "object type not found: {}",
338 obj_type
339 )))?;
340 Ok(if matches!(entry, ObjectEntry::Object(_)) {
341 self.site
342 .manifest
343 .objects_dir
344 .join(Path::new(&format!("{}.toml", obj_type)))
345 } else {
346 self.site
347 .manifest
348 .objects_dir
349 .join(Path::new(&obj_type))
350 .join(Path::new(&format!("{}.toml", filename)))
351 })
352 }
353 pub fn object_file(&self, obj_type: &str, filename: &str) -> Result<String> {
354 self.fs_mutex
355 .with_fs(|fs| self.modify_object_file(obj_type, filename, |o| Ok(o), fs))
356 }
357 pub fn sha_for_file(&self, file: &Path) -> Result<String> {
358 let file_data = self
359 .fs_mutex
360 .with_fs(|fs| fs.read(file))?
361 .ok_or_else(|| ArchivalError::new("failed generating sha"))?;
362 Ok(sha_for_data(&file_data))
363 }
364
365 pub fn write_file(&self, obj_type: &str, filename: &str, contents: String) -> Result<()> {
366 let obj_def = self.get_object_definition(obj_type)?;
368 let table: toml::Table = toml::from_str(&contents)?;
369 let _ = Object::from_table(
371 obj_def,
372 Path::new(filename),
373 &table,
374 &self.site.manifest.editor_types,
375 false,
376 )?;
377 self.fs_mutex
379 .with_fs(|fs| fs.write_str(&self.object_path_impl(obj_type, filename, fs)?, contents))
380 }
381 fn modify_object_file(
382 &self,
383 obj_type: &str,
384 filename: &str,
385 obj_cb: impl FnOnce(&mut Object) -> Result<&mut Object>,
386 fs: &F,
387 ) -> Result<String> {
388 let mut all_objects = self.site.get_objects(fs)?;
389 let definitions = &self.site.object_definitions;
390 if let Some(objects) = all_objects.get_mut(obj_type) {
391 if let Some(object) = objects.iter_mut().find(|o| o.filename == filename) {
392 let object = obj_cb(object)?;
393 let def = definitions.get(obj_type).ok_or_else(|| {
394 ArchivalError::new(&format!("missing object definition: {obj_type}"))
395 })?;
396 Ok(object.to_toml(def)?)
397 } else {
398 Err(objects
399 .as_list()
400 .map(|list| {
401 ArchivalError::new(&format!(
402 "{} {} not found in [{}]",
403 obj_type,
404 filename,
405 list.iter()
406 .map(|o| o.filename.clone())
407 .collect::<Vec<_>>()
408 .join(", ")
409 ))
410 .into()
411 })
412 .unwrap_or_else(|| {
413 ArchivalError::new(&format!("object {} not found", filename)).into()
414 }))
415 }
416 } else {
417 Err(ArchivalError::new(&format!("no objects of type: {}", obj_type)).into())
418 }
419 }
420
421 pub fn send_event(
422 &self,
423 event: ArchivalEvent,
424 build_options: Option<BuildOptions>,
425 ) -> Result<ArchivalEventResponse> {
426 let r = match event {
427 ArchivalEvent::AddObject(event) => self.add_object(event)?,
428 ArchivalEvent::RenameObject(event) => self.rename_object(event)?,
429 ArchivalEvent::AddRootObject(event) => self.add_root_object(event)?,
430 ArchivalEvent::DeleteObject(event) => self.delete_object(event)?,
431 ArchivalEvent::EditField(event) => self.edit_field(event)?,
432 ArchivalEvent::EditOrder(event) => self.edit_order(event)?,
433 ArchivalEvent::AddChild(event) => self.add_child(event)?,
434 ArchivalEvent::RemoveChild(event) => self.remove_child(event)?,
435 };
436 if let Some(build_options) = build_options {
437 self.build(build_options)?;
438 }
439 Ok(r)
440 }
441
442 fn add_root_object(&self, event: AddRootObjectEvent) -> Result<ArchivalEventResponse> {
444 let obj_def = self.get_object_definition(&event.object)?;
445 self.fs_mutex.with_fs(|fs| {
446 let dir_path = self
447 .site
448 .manifest
449 .objects_dir
450 .join(Path::new(&event.object));
451 if fs.is_dir(&dir_path)? && fs.walk_dir(&dir_path, false)?.next().is_some() {
452 return Err(ArchivalError::new(&format!(
453 "cannod add root {} object, found existing non-roots.",
454 event.object
455 ))
456 .into());
457 }
458 let path = self
459 .site
460 .manifest
461 .objects_dir
462 .join(Path::new(&format!("{}.toml", event.object)));
463 if fs.exists(&path)? {
464 return Err(ArchivalError::new(&format!(
465 "cannod add root {}, file already exists.",
466 event.object
467 ))
468 .into());
469 }
470 let object = Object::from_def(obj_def, &event.object, None, event.values)?;
471 fs.write_str(&path, object.to_toml(obj_def)?)?;
472 self.site.invalidate_file(&path);
473 Ok(())
474 })?;
475 Ok(ArchivalEventResponse::None)
476 }
477
478 fn add_object(&self, event: AddObjectEvent) -> Result<ArchivalEventResponse> {
479 let obj_def = self.get_object_definition(&event.object)?;
480 self.fs_mutex.with_fs(|fs| {
481 let obj_dir = self
482 .site
483 .manifest
484 .objects_dir
485 .join(Path::new(&event.object));
486 fs.create_dir_all(&obj_dir)?;
487 let path = obj_dir.join(Path::new(&format!("{}.toml", event.filename)));
488 if fs.exists(&path)? {
489 return Err(ArchivalError::new(&format!(
490 "cannod add {} named {}, file already exists.",
491 event.object, event.filename
492 ))
493 .into());
494 }
495 let root_path = self
496 .site
497 .manifest
498 .objects_dir
499 .join(Path::new(&format!("{}.toml", event.object)));
500 if fs.exists(&root_path)? {
501 return Err(ArchivalError::new(&format!(
502 "cannod add {} named {}, there's already a root {}.",
503 event.object, event.filename, event.object
504 ))
505 .into());
506 }
507 let object = Object::from_def(obj_def, &event.filename, event.order, event.values)?;
508 fs.write_str(&path, object.to_toml(obj_def)?)
509 .map_err(|error| {
510 ArchivalError::new(&format!("failed writing to {}: {}", path.display(), error))
511 })?;
512 self.site.invalidate_file(&path);
513 Ok(())
514 })?;
515 Ok(ArchivalEventResponse::None)
516 }
517
518 fn rename_object(&self, event: RenameObjectEvent) -> Result<ArchivalEventResponse> {
519 let obj_def = self.get_object_definition(&event.object)?;
520 self.fs_mutex.with_fs(|fs| {
521 let root_objects = self.site.root_objects(fs);
522 if root_objects.contains(&event.from) {
523 return Err(ArchivalError::new(&format!(
524 "cannot rename root object {}",
525 event.from
526 ))
527 .into());
528 }
529 let from_path = self.object_path_impl(&obj_def.name, &event.from, fs)?;
530 let to_path = self.object_path_impl(&obj_def.name, &event.to, fs)?;
531 let content = fs.read(&from_path)?.ok_or(ArchivalError::new(&format!(
532 "file not found: {}",
533 event.from
534 )))?;
535 fs.write(&to_path, content)?;
536 fs.delete(&from_path)?;
537 self.site.invalidate_file(&from_path);
538 Ok(())
539 })?;
540 Ok(ArchivalEventResponse::None)
541 }
542
543 fn delete_object(&self, event: DeleteObjectEvent) -> Result<ArchivalEventResponse> {
544 let obj_def = self.get_object_definition(&event.object)?;
545 self.fs_mutex.with_fs(|fs| {
546 let path = self.object_path_impl(&obj_def.name, &event.filename, fs)?;
547 fs.delete(&path)?;
548 self.site.invalidate_file(&path);
549 Ok(())
550 })?;
551 Ok(ArchivalEventResponse::None)
552 }
553
554 pub fn manifest_content(&self) -> Result<String> {
555 self.fs_mutex.with_fs(|fs| self.site.manifest_content(fs))
556 }
557
558 pub fn get_object_definition(&self, name: &str) -> Result<&ObjectDefinition, ArchivalError> {
559 self.site
560 .object_definitions
561 .get(name)
562 .ok_or(ArchivalError::new(&format!("object not found: {}", name)))
563 }
564
565 pub fn get_objects(&self) -> Result<ObjectMap> {
566 self.fs_mutex.with_fs(|fs| self.site.get_objects(fs))
567 }
568
569 pub fn get_object(&self, name: &str, filename: Option<&str>) -> Result<Object> {
570 self.fs_mutex
571 .with_fs(|fs| self.site.get_object(name, filename, fs))
572 }
573
574 pub fn get_objects_sorted(
575 &self,
576 sort: impl Fn(&Object, &Object) -> Ordering,
577 ) -> Result<ObjectMap> {
578 self.fs_mutex
579 .with_fs(|fs| self.site.get_objects_sorted(fs, Some(sort)))
580 }
581
582 pub fn get_rendered_objects(&self) -> Result<RenderedObjectMap> {
583 self.fs_mutex
584 .with_fs(|fs| self.site.get_rendered_objects(fs))
585 }
586
587 pub fn get_rendered_object(
588 &self,
589 name: &str,
590 filename: Option<&str>,
591 ) -> Result<RenderedObject> {
592 self.fs_mutex
593 .with_fs(|fs| self.site.get_rendered_object(name, filename, fs))
594 }
595
596 pub fn get_rendered_objects_sorted(
597 &self,
598 sort: impl Fn(&Object, &Object) -> Ordering,
599 ) -> Result<RenderedObjectMap> {
600 self.fs_mutex
601 .with_fs(|fs| self.site.get_rendered_objects_sorted(fs, Some(sort)))
602 }
603
604 fn edit_field(&self, event: EditFieldEvent) -> Result<ArchivalEventResponse> {
605 let def = self
606 .site
607 .object_definitions
608 .get(&event.object)
609 .ok_or_else(|| {
610 ArchivalError::new(&format!("object type not found: {}", event.object))
611 })?;
612 if let Some(value) = &event.value {
613 value.validate(
614 &event
615 .path
616 .clone()
617 .concat(ValuePath::from_string(&event.field)),
618 def,
619 &self.site.manifest.editor_types,
620 )?;
621 }
622 self.write_object(&event.object, &event.filename, |existing| {
623 event
624 .path
625 .append((&(*event.field)).into())
626 .set_in_object(existing, event.value)?;
627 Ok(existing)
628 })?;
629 Ok(ArchivalEventResponse::None)
630 }
631 fn edit_order(&self, event: EditOrderEvent) -> Result<ArchivalEventResponse> {
632 self.write_object(&event.object, &event.filename, |existing| {
633 existing.order = event.order;
634 Ok(existing)
635 })?;
636 Ok(ArchivalEventResponse::None)
637 }
638
639 fn add_child(&self, event: AddChildEvent) -> Result<ArchivalEventResponse> {
640 let mut added_idx = usize::MAX;
641 let def = self
642 .site
643 .object_definitions
644 .get(&event.object)
645 .ok_or_else(|| {
646 ArchivalError::new(&format!("object type not found: {}", event.object))
647 })?;
648 for value in &event.values {
650 value.value.validate(
651 &event.path.clone().concat(value.path.clone()),
652 def,
653 &self.site.manifest.editor_types,
654 )?;
655 }
656 let child_def = event.path.get_definition(def)?;
661 self.write_object(&event.object, &event.filename, |existing| {
662 added_idx = event.path.add_child(existing, event.index, |child| {
663 *child = child_def.empty_object();
664 for value in event.values {
665 value.path.set_in_tree(child, Some(value.value))?;
666 }
667 Ok(())
668 })?;
669 Ok(existing)
670 })?;
671 Ok(ArchivalEventResponse::Index(added_idx))
672 }
673 fn remove_child(&self, event: RemoveChildEvent) -> Result<ArchivalEventResponse> {
674 self.write_object(&event.object, &event.filename, move |existing| {
675 let mut path = event.path;
676 path.remove_child(existing)?;
677 Ok(existing)
678 })?;
679 Ok(ArchivalEventResponse::None)
680 }
681
682 fn write_object(
683 &self,
684 obj_type: &str,
685 filename: &str,
686 obj_cb: impl FnOnce(&mut Object) -> Result<&mut Object>,
687 ) -> Result<()> {
688 debug!("write object {}: {}", obj_type, filename);
689 self.fs_mutex.with_fs(|fs| {
690 let path = self.object_path_impl(obj_type, filename, fs)?;
691 let contents = self.modify_object_file(obj_type, filename, obj_cb, fs)?;
692 fs.write_str(&path, contents)?;
693 self.site.invalidate_file(&path);
694 Ok(())
695 })
696 }
697
698 pub fn modify_manifest(&mut self, modify: impl FnOnce(&mut Manifest)) -> Result<()> {
699 self.fs_mutex.with_fs(|fs| {
700 self.site.modify_manifest(fs, modify)?;
701 Ok(())
702 })
703 }
704
705 pub fn delete_objects(
710 &self,
711 object_names: impl IntoIterator<Item = impl AsRef<str>>,
712 keep_objects: Option<Vec<ValuePath>>,
713 ) -> Result<()> {
714 let objects = self.get_objects()?;
715 self.fs_mutex.with_fs(|fs| {
716 for on in object_names {
717 let object_name = on.as_ref();
718 let current_path: ValuePath =
719 ValuePath::empty().append(ValuePath::key(object_name));
720 if keep_objects
721 .as_ref()
722 .is_some_and(|ko| ko.contains(¤t_path))
723 {
724 continue;
725 }
726 let entry = objects.get(object_name).ok_or_else(|| {
727 ArchivalError::new(&format!("object {} does not exist", object_name))
728 })?;
729 let filenames = match entry {
730 ObjectEntry::Object(object) => {
731 vec![&object.filename]
732 }
733 ObjectEntry::List(objects) => objects.iter().map(|o| &o.filename).collect(),
734 };
735 for filename in filenames {
736 let current_path = current_path.clone().append(ValuePath::key(filename));
737 if keep_objects
738 .as_ref()
739 .is_some_and(|ko| ko.contains(¤t_path))
740 {
741 continue;
742 }
743 let path = self.object_path_impl(object_name, filename, fs)?;
744 fs.delete(&path)?;
745 self.site.invalidate_file(&path);
746 }
747 }
748 Ok(())
749 })
750 }
751
752 pub fn take_fs(self) -> F {
753 self.fs_mutex.take_fs()
754 }
755 pub fn clone_fs(&self) -> Result<F> {
756 self.fs_mutex.with_fs(|fs| Ok(fs.clone()))
757 }
758}
759
760#[cfg(test)]
761mod lib {
762 use anyhow::Result;
763
764 use crate::{file_system::unpack_zip, test_utils::as_path_str, value_path::ValuePath};
765 use events::AddObjectValue;
766 use tracing_test::traced_test;
767
768 use super::*;
769
770 #[test]
771 #[traced_test]
772 fn load_and_build_site_from_zip() -> Result<()> {
773 let mut fs = MemoryFileSystem::default();
774 let zip = include_bytes!("../tests/fixtures/archival-website.zip");
775 unpack_zip(zip.to_vec(), &mut fs)?;
776 let archival = Archival::new(fs)?;
777 assert_eq!(archival.site.object_definitions.len(), 4);
778 assert!(archival.site.object_definitions.contains_key("section"));
779 assert!(archival.site.object_definitions.contains_key("post"));
780 assert!(archival.site.object_definitions.contains_key("site"));
781 let objects = archival.get_objects()?;
782 let section_objs = objects.get("section").unwrap();
783 assert!(matches!(section_objs, ObjectEntry::List(_)));
784 let site_obj = objects.get("site").unwrap();
785 assert!(matches!(site_obj, ObjectEntry::Object(_)));
786 let post_obj = objects.get("post").unwrap();
787 assert!(matches!(post_obj, ObjectEntry::List(_)));
788 let fp = post_obj.into_iter().next().unwrap();
789 let m = ValuePath::from_string("media").get_in_object(fp).unwrap();
790 assert!(matches!(m, FieldValue::Oneof(..)));
791 let fc = &archival.site.field_config;
792 if let FieldValue::Oneof((t, val)) = m {
793 assert_eq!(t, "image");
794 assert!(matches!(**val, Some(FieldValue::File(_))));
795 if let Some(FieldValue::File(img)) = val.as_ref() {
796 assert_eq!(img.filename, "test.jpg");
797 assert_eq!(img.mime, "image/jpg");
798 assert_eq!(img.name, Some("Test".to_string()));
799 assert_eq!(img.sha, "test-sha");
800 assert_eq!(img.url(fc), "test://uploads-url/test-sha/test.jpg");
801 }
802 }
803 archival.build(BuildOptions::default())?;
804 let dist_files = archival
805 .dist_files()
806 .into_iter()
807 .map(|f| f.path.display().to_string())
808 .collect::<Vec<String>>();
809 println!("dist_files: \n{:#?}", dist_files);
810 assert!(dist_files.contains(&as_path_str("index.html")));
811 assert!(dist_files.contains(&as_path_str("404.html")));
812 assert!(dist_files.contains(&as_path_str("post/a-post.html")));
813 assert!(dist_files.contains(&as_path_str("img/guy.webp")));
814 assert!(dist_files.contains(&as_path_str("rss.rss")));
815 assert_eq!(dist_files.len(), 17);
816 let guy = archival.dist_file(Path::new("img/guy.webp"));
817 assert!(guy.is_some());
818 let post_html = archival
819 .fs_mutex
820 .with_fs(|fs| {
821 fs.read_to_string(archival.site.manifest.build_dir.join("post/a-post.html"))
822 })?
823 .unwrap();
824 println!("{}", post_html);
825 assert!(post_html.contains("test://uploads-url/test-sha/test.jpg"));
826 assert!(post_html.contains("title=\"Test\""));
827 Ok(())
828 }
829
830 #[test]
831 #[traced_test]
832 fn definitions_carry_their_objects_toml_comments() -> Result<()> {
833 let mut fs = MemoryFileSystem::default();
834 let zip = include_bytes!("../tests/fixtures/archival-website.zip");
835 unpack_zip(zip.to_vec(), &mut fs)?;
836 let archival = Archival::new(fs)?;
837
838 let post = archival.get_object_definition("post")?;
839 assert_eq!(post.description, Some("An entry on the blog.".to_string()));
840
841 let described = |field: &str| post.fields.get(field).unwrap().description.clone();
842 assert_eq!(
843 described("title"),
844 Some("The post's headline.\nUsed for the page title as well.".to_string())
845 );
846 assert_eq!(
848 described("state"),
849 Some("Whether the post is visible on the site.".to_string())
850 );
851 assert_eq!(
853 described("media"),
854 Some("A single piece of media to show alongside the post.".to_string())
855 );
856 assert_eq!(
858 post.children.get("links").unwrap().description,
859 Some("Related links to show at the end of the post.".to_string())
860 );
861
862 let section = archival.get_object_definition("section")?;
865 assert_eq!(section.description, None);
866 assert_eq!(section.fields.get("name").unwrap().description, None);
867 Ok(())
868 }
869
870 #[test]
871 fn add_object_to_site() -> Result<()> {
872 let mut fs = MemoryFileSystem::default();
873 let zip = include_bytes!("../tests/fixtures/archival-website.zip");
874 unpack_zip(zip.to_vec(), &mut fs)?;
875 let archival = Archival::new(fs)?;
876 archival.send_event(
877 ArchivalEvent::AddObject(AddObjectEvent {
878 object: "section".to_string(),
879 filename: "my-section".to_string(),
880 order: Some(3.),
881 values: vec![AddObjectValue {
883 path: ValuePath::from_string("name"),
884 value: FieldValue::String("section three".to_string()),
885 }],
886 }),
887 Some(BuildOptions::default()),
888 )?;
889 let sections_dir = archival.site.manifest.objects_dir.join("section");
891 let sections = archival.fs_mutex.with_fs(|fs| {
892 fs.walk_dir(§ions_dir, false)
893 .map(|d| d.collect::<Vec<PathBuf>>())
894 })?;
895 println!("SECTIONS: {:?}", sections);
896 assert_eq!(sections.len(), 3);
897 let section_toml = archival
898 .fs_mutex
899 .with_fs(|fs| fs.read_to_string(sections_dir.join("my-section.toml")));
900 assert!(section_toml.is_ok());
901 let index_html = archival
902 .fs_mutex
903 .with_fs(|fs| fs.read_to_string(archival.site.manifest.build_dir.join("index.html")))?
904 .unwrap();
905 let rendered_sections: Vec<_> = index_html.match_indices("<h2>").collect();
906 println!("MATCHED: {:?}", rendered_sections);
907 assert_eq!(rendered_sections.len(), 3);
908 Ok(())
909 }
910
911 #[test]
912 fn edit_object() -> Result<()> {
913 let mut fs = MemoryFileSystem::default();
914 let zip = include_bytes!("../tests/fixtures/archival-website.zip");
915 unpack_zip(zip.to_vec(), &mut fs)?;
916 let archival = Archival::new(fs)?;
917 archival.send_event(
918 ArchivalEvent::EditField(EditFieldEvent {
919 object: "section".to_string(),
920 filename: "first".to_string(),
921 path: ValuePath::empty(),
922 field: "name".to_string(),
923 value: Some(FieldValue::String("This is the new name".to_string())),
924 source: None,
925 }),
926 Some(BuildOptions::default()),
927 )?;
928 let index_html = archival
930 .fs_mutex
931 .with_fs(|fs| fs.read_to_string(archival.site.manifest.build_dir.join("index.html")))?
932 .unwrap();
933 println!("index: {}", index_html);
934 assert!(index_html.contains("This is the new name"));
935 Ok(())
936 }
937
938 #[test]
939 fn delete_object() -> Result<()> {
940 let mut fs = MemoryFileSystem::default();
941 let zip = include_bytes!("../tests/fixtures/archival-website.zip");
942 unpack_zip(zip.to_vec(), &mut fs)?;
943 let archival = Archival::new(fs)?;
944 archival.send_event(
945 ArchivalEvent::DeleteObject(DeleteObjectEvent {
946 object: "section".to_string(),
947 filename: "first".to_string(),
948 source: None,
949 }),
950 Some(BuildOptions::default()),
951 )?;
952 let sections_dir = archival.site.manifest.objects_dir.join("section");
954 let sections = archival.fs_mutex.with_fs(|fs| {
955 fs.walk_dir(§ions_dir, false)
956 .map(|d| d.collect::<Vec<PathBuf>>())
957 })?;
958 println!("SECTIONS: {:?}", sections);
959 assert_eq!(sections.len(), 1);
960 let index_html = archival
961 .fs_mutex
962 .with_fs(|fs| fs.read_to_string(archival.site.manifest.build_dir.join("index.html")))?
963 .unwrap();
964 println!("index: {}", index_html);
965 assert!(!index_html.contains("This is the new title"));
966 Ok(())
967 }
968
969 #[test]
970 fn rename_object() -> Result<()> {
971 let mut fs = MemoryFileSystem::default();
972 let zip = include_bytes!("../tests/fixtures/archival-website.zip");
973 unpack_zip(zip.to_vec(), &mut fs)?;
974 let archival = Archival::new(fs)?;
975 let sections_dir = archival.site.manifest.objects_dir.join("section");
976 let sections_before_rename = archival.fs_mutex.with_fs(|fs| {
977 fs.walk_dir(§ions_dir, false)
978 .map(|d| d.collect::<Vec<PathBuf>>())
979 })?;
980 archival.send_event(
981 ArchivalEvent::RenameObject(RenameObjectEvent {
982 object: "section".to_string(),
983 from: "first".to_string(),
984 to: "renamed".to_string(),
985 }),
986 Some(BuildOptions::default()),
987 )?;
988 let sections_dir = archival.site.manifest.objects_dir.join("section");
990 let sections = archival.fs_mutex.with_fs(|fs| {
991 fs.walk_dir(§ions_dir, false)
992 .map(|d| d.collect::<Vec<PathBuf>>())
993 })?;
994 println!("SECTIONS: {:?}", sections);
995 assert_eq!(sections.len(), sections_before_rename.len());
996 assert!(sections.iter().any(|path| path.ends_with("renamed.toml")));
997 Ok(())
998 }
999
1000 #[test]
1001 fn rename_object_with_modifications() -> Result<()> {
1002 let mut fs = MemoryFileSystem::default();
1003 let zip = include_bytes!("../tests/fixtures/archival-website.zip");
1004 unpack_zip(zip.to_vec(), &mut fs)?;
1005 let archival = Archival::new(fs)?;
1006 let sections_dir = archival.site.manifest.objects_dir.join("section");
1007 let sections_before_rename = archival.fs_mutex.with_fs(|fs| {
1008 fs.walk_dir(§ions_dir, false)
1009 .map(|d| d.collect::<Vec<PathBuf>>())
1010 })?;
1011 archival.send_event(
1012 ArchivalEvent::EditField(EditFieldEvent {
1013 object: "section".to_string(),
1014 filename: "first".to_string(),
1015 path: ValuePath::empty(),
1016 field: "name".to_string(),
1017 value: Some(FieldValue::String("This is the new name".to_string())),
1018 source: None,
1019 }),
1020 Some(BuildOptions::default()),
1021 )?;
1022 archival.send_event(
1023 ArchivalEvent::RenameObject(RenameObjectEvent {
1024 object: "section".to_string(),
1025 from: "first".to_string(),
1026 to: "renamed".to_string(),
1027 }),
1028 Some(BuildOptions::default()),
1029 )?;
1030 let sections_dir = archival.site.manifest.objects_dir.join("section");
1032 let sections = archival.fs_mutex.with_fs(|fs| {
1033 fs.walk_dir(§ions_dir, false)
1034 .map(|d| d.collect::<Vec<PathBuf>>())
1035 })?;
1036 println!("SECTIONS: {:?}", sections);
1037 assert_eq!(sections.len(), sections_before_rename.len());
1038 assert!(sections.iter().any(|path| path.ends_with("renamed.toml")));
1039 let renamed = archival
1040 .get_object("section", Some("renamed"))
1041 .expect("missing renamed object");
1042 let name = ValuePath::from_string("name")
1043 .get_in_object(&renamed)
1044 .expect("missing name");
1045 if let FieldValue::String(name) = name {
1046 assert_eq!(name, "This is the new name");
1047 } else {
1048 panic!("name not string");
1049 }
1050 archival.send_event(
1051 ArchivalEvent::EditField(EditFieldEvent {
1052 object: "section".to_string(),
1053 filename: "renamed".to_string(),
1054 path: ValuePath::empty(),
1055 field: "name".to_string(),
1056 value: Some(FieldValue::String("This is another name".to_string())),
1057 source: None,
1058 }),
1059 Some(BuildOptions::default()),
1060 )?;
1061 let renamed = archival
1062 .get_object("section", Some("renamed"))
1063 .expect("missing renamed object");
1064 let name = ValuePath::from_string("name")
1065 .get_in_object(&renamed)
1066 .expect("missing name");
1067 if let FieldValue::String(name) = name {
1068 assert_eq!(name, "This is another name");
1069 } else {
1070 panic!("name not string");
1071 }
1072 Ok(())
1073 }
1074
1075 #[test]
1076 #[traced_test]
1077 fn edit_object_order() -> Result<()> {
1078 let mut fs = MemoryFileSystem::default();
1079 let zip = include_bytes!("../tests/fixtures/archival-website.zip");
1080 unpack_zip(zip.to_vec(), &mut fs)?;
1081 let archival = Archival::new(fs)?;
1082 archival.build(BuildOptions::default())?;
1083 let index_html = archival
1084 .fs_mutex
1085 .with_fs(|fs| fs.read_to_string(archival.site.manifest.build_dir.join("index.html")))?
1086 .unwrap();
1087 println!("index: {}", index_html);
1088 let c1 = index_html.find("1 Some Content").unwrap();
1089 let c2 = index_html.find("2 More Content").unwrap();
1090 assert!(c1 < c2);
1091 archival.send_event(
1092 ArchivalEvent::EditOrder(EditOrderEvent {
1093 object: "section".to_string(),
1094 filename: "first".to_string(),
1095 order: Some(12.),
1096 source: None,
1097 }),
1098 Some(BuildOptions::default()),
1099 )?;
1100 let index_html = archival
1102 .fs_mutex
1103 .with_fs(|fs| fs.read_to_string(archival.site.manifest.build_dir.join("index.html")))?
1104 .unwrap();
1105 println!("index: {}", index_html);
1106 let c1 = index_html.find("12 Some Content").unwrap();
1107 let c2 = index_html.find("2 More Content").unwrap();
1108 assert!(c2 < c1);
1109 Ok(())
1110 }
1111
1112 #[test]
1113 #[traced_test]
1114 fn add_child() {
1115 let mut fs = MemoryFileSystem::default();
1116 let zip = include_bytes!("../tests/fixtures/archival-website.zip");
1117 unpack_zip(zip.to_vec(), &mut fs).unwrap();
1118 let archival = Archival::new(fs).unwrap();
1119 archival.build(BuildOptions::default()).unwrap();
1120 let post_html = archival
1121 .fs_mutex
1122 .with_fs(|fs| {
1123 fs.read_to_string(
1124 archival
1125 .site
1126 .manifest
1127 .build_dir
1128 .join(Path::new("post/a-post.html")),
1129 )
1130 })
1131 .unwrap()
1132 .unwrap();
1133 let rendered_links: Vec<_> = post_html.match_indices("<a href=").collect();
1135 assert_eq!(rendered_links.len(), 2);
1137 archival
1138 .send_event(
1139 ArchivalEvent::AddChild(AddChildEvent {
1140 object: "post".to_string(),
1141 filename: "a-post".to_string(),
1142 path: ValuePath::default().append(ValuePath::key("links")),
1143 values: vec![
1144 AddObjectValue {
1145 path: ValuePath::from_string("url"),
1146 value: FieldValue::String("http://foo.com".to_string()),
1147 },
1148 AddObjectValue {
1149 path: ValuePath::from_string("name"),
1150 value: FieldValue::String("another link".to_string()),
1151 },
1152 ],
1153 index: None,
1154 }),
1155 Some(BuildOptions::no_static()),
1156 )
1157 .unwrap();
1158 let objects = archival.get_objects().unwrap();
1159 let posts = objects.get("post").unwrap();
1160 let mut found = false;
1161 for post in posts {
1162 if post.filename == "a-post" {
1163 found = true;
1164 let links = ValuePath::from_string("links").get_in_object(post).unwrap();
1165 assert!(matches!(links, FieldValue::Objects(_)));
1166 if let FieldValue::Objects(links) = links {
1167 assert_eq!(links.len(), 3);
1168 }
1169 }
1170 }
1171 assert!(found, "a-post not found in posts: {:?}", posts);
1172 let post_html = archival
1174 .fs_mutex
1175 .with_fs(|fs| {
1176 fs.read_to_string(
1177 archival
1178 .site
1179 .manifest
1180 .build_dir
1181 .join(Path::new("post/a-post.html")),
1182 )
1183 })
1184 .unwrap()
1185 .unwrap();
1186 println!("post: {}", post_html);
1187 let rendered_links: Vec<_> = post_html.match_indices("<a href=").collect();
1188 println!("LINKS: {:?}", rendered_links);
1189 assert_eq!(rendered_links.len(), 3);
1190 let rendered_link_url: Vec<_> = post_html.match_indices("foo.com").collect();
1191 assert_eq!(rendered_link_url.len(), 1);
1192 let rendered_link_name: Vec<_> = post_html.match_indices("another link").collect();
1193 assert_eq!(rendered_link_name.len(), 1);
1194 }
1195
1196 #[test]
1197 fn add_child_after_add_object() -> Result<()> {
1198 let mut fs = MemoryFileSystem::default();
1199 let zip = include_bytes!("../tests/fixtures/archival-website.zip");
1200 unpack_zip(zip.to_vec(), &mut fs)?;
1201 let archival = Archival::new(fs)?;
1202 archival.build(BuildOptions::default())?;
1203 archival.send_event(
1204 ArchivalEvent::AddObject(AddObjectEvent {
1205 object: "post".to_string(),
1206 filename: "new-name".to_string(),
1207 order: None,
1208 values: vec![
1209 AddObjectValue {
1210 path: ValuePath::from_string("title"),
1211 value: FieldValue::String("new-name".to_string()),
1212 },
1213 AddObjectValue {
1214 path: ValuePath::from_string("date"),
1215 value: FieldValue::Date(fields::DateTime::from_ymd(2024, 4, 22)),
1216 },
1217 AddObjectValue {
1218 path: ValuePath::from_string("content"),
1219 value: FieldValue::Markdown("content".to_string()),
1220 },
1221 ],
1222 }),
1223 None,
1224 )?;
1225
1226 let result = archival.send_event(
1227 ArchivalEvent::AddChild(AddChildEvent {
1228 object: "post".to_string(),
1229 filename: "new-name".to_string(),
1230 path: ValuePath::default().append(ValuePath::key("links")),
1231 values: vec![
1232 AddObjectValue {
1233 path: ValuePath::from_string("name"),
1234 value: FieldValue::String("added child".to_string()),
1235 },
1236 AddObjectValue {
1237 path: ValuePath::from_string("url"),
1238 value: FieldValue::String("https://foo.test".to_string()),
1239 },
1240 ],
1241 index: None,
1242 }),
1243 None,
1244 )?;
1245 assert!(matches!(result, ArchivalEventResponse::Index(0)));
1246
1247 let post = archival.get_object("post", Some("new-name"))?;
1248 let links = ValuePath::from_string("links")
1249 .get_in_object(&post)
1250 .unwrap();
1251 assert!(matches!(links, FieldValue::Objects(_)));
1252 if let FieldValue::Objects(links) = links {
1253 assert_eq!(links.len(), 1);
1254 }
1255
1256 Ok(())
1257 }
1258
1259 #[test]
1260 fn remove_child() -> Result<()> {
1261 let mut fs = MemoryFileSystem::default();
1262 let zip = include_bytes!("../tests/fixtures/archival-website.zip");
1263 unpack_zip(zip.to_vec(), &mut fs)?;
1264 let archival = Archival::new(fs)?;
1265 archival
1266 .send_event(
1267 ArchivalEvent::RemoveChild(RemoveChildEvent {
1268 object: "post".to_string(),
1269 filename: "a-post".to_string(),
1270 path: ValuePath::default()
1271 .append(ValuePath::key("links"))
1272 .append(ValuePath::index(0)),
1273 source: None,
1274 }),
1275 Some(BuildOptions::default()),
1276 )
1277 .unwrap();
1278 let post_html = archival
1280 .fs_mutex
1281 .with_fs(|fs| {
1282 fs.read_to_string(
1283 archival
1284 .site
1285 .manifest
1286 .build_dir
1287 .join(Path::new("post/a-post.html")),
1288 )
1289 })?
1290 .unwrap();
1291 println!("post: {}", post_html);
1292 let rendered_links: Vec<_> = post_html.match_indices("<a href=").collect();
1293 println!("LINKS: {:?}", rendered_links);
1294 assert_eq!(rendered_links.len(), 1);
1295 Ok(())
1296 }
1297
1298 #[test]
1299 fn modify_manifest() -> Result<()> {
1300 let mut fs = MemoryFileSystem::default();
1301 let zip = include_bytes!("../tests/fixtures/archival-website.zip");
1302 unpack_zip(zip.to_vec(), &mut fs)?;
1303 let mut archival = Archival::new(fs)?;
1304 archival.modify_manifest(|m| {
1305 m.site_url = Some("test.com".to_string());
1306 m.prebuild = vec!["test".to_string()];
1307 })?;
1308 let output = archival.site.manifest.to_toml()?;
1309 println!("{}", output);
1310 assert!(output.contains("site_url = \"test.com\""));
1311 assert!(!output.contains("objects_dir"));
1313 assert!(!output.contains("objects"));
1314 assert!(output.contains("prebuild = [\"test\"]"));
1316 Ok(())
1317 }
1318
1319 #[test]
1320 fn bulk_delete_objects() -> Result<()> {
1321 let mut fs = MemoryFileSystem::default();
1322 let zip = include_bytes!("../tests/fixtures/archival-website.zip");
1323 unpack_zip(zip.to_vec(), &mut fs)?;
1324 let archival = Archival::new(fs)?;
1325 archival.delete_objects(
1326 vec!["section", "site"],
1327 Some(vec![ValuePath::from_string("section.second")]),
1328 )?;
1329 let sections_dir = archival.site.manifest.objects_dir.join("section");
1331 let sections = archival.fs_mutex.with_fs(|fs| {
1332 fs.walk_dir(§ions_dir, false)
1333 .map(|d| d.collect::<Vec<PathBuf>>())
1334 })?;
1335 println!("SECTIONS: {:?}", sections);
1336 assert_eq!(sections.len(), 1);
1337 let site_file_exists = archival
1338 .fs_mutex
1339 .with_fs(|fs| fs.exists(archival.site.manifest.objects_dir.join("site.toml")))?;
1340 assert!(!site_file_exists);
1341 Ok(())
1342 }
1343
1344 #[test]
1345 fn edit_enum() -> Result<()> {
1346 let mut fs = MemoryFileSystem::default();
1347 let zip = include_bytes!("../tests/fixtures/archival-website.zip");
1348 unpack_zip(zip.to_vec(), &mut fs)?;
1349 let archival = Archival::new(fs)?;
1350 archival.send_event(
1351 ArchivalEvent::EditField(EditFieldEvent {
1352 object: "post".to_string(),
1353 filename: "a-post".to_string(),
1354 path: ValuePath::empty(),
1355 field: "state".to_string(),
1356 value: Some(FieldValue::Enum("draft".to_string())),
1357 source: None,
1358 }),
1359 Some(BuildOptions::default()),
1360 )?;
1361 let post_html = archival
1362 .fs_mutex
1363 .with_fs(|fs| {
1364 fs.read_to_string(
1365 archival
1366 .site
1367 .manifest
1368 .build_dir
1369 .join(Path::new("post/a-post.html")),
1370 )
1371 })?
1372 .unwrap();
1373 println!("post: {}", post_html);
1374 assert!(post_html.contains("State: draft"));
1375 Ok(())
1376 }
1377 #[test]
1378 fn edit_enum_fails_when_invalid() -> Result<()> {
1379 let mut fs = MemoryFileSystem::default();
1380 let zip = include_bytes!("../tests/fixtures/archival-website.zip");
1381 unpack_zip(zip.to_vec(), &mut fs)?;
1382 let archival = Archival::new(fs)?;
1383 assert!(archival
1384 .send_event(
1385 ArchivalEvent::EditField(EditFieldEvent {
1386 object: "post".to_string(),
1387 filename: "a-post".to_string(),
1388 path: ValuePath::empty(),
1389 field: "state".to_string(),
1390 value: Some(FieldValue::Enum("poopoo".to_string())),
1391 source: None,
1392 }),
1393 Some(BuildOptions::default()),
1394 )
1395 .is_err());
1396 Ok(())
1397 }
1398
1399 fn named_site_fs(manifest: &str, objects: &str, site_url: &str) -> Result<MemoryFileSystem> {
1400 let mut fs = MemoryFileSystem::default();
1401 fs.write_str(
1402 manifest,
1403 format!("upload_prefix = \"\"\nsite_url = \"{}\"\n", site_url),
1404 )?;
1405 fs.write_str(objects, "[site]\nname = \"string\"\n".to_string())?;
1406 Ok(fs)
1407 }
1408
1409 #[test]
1410 fn legacy_file_names_are_still_supported() -> Result<()> {
1411 let fs = named_site_fs(
1412 LEGACY_MANIFEST_FILE_NAME,
1413 LEGACY_OBJECT_DEFINITION_FILE_NAME,
1414 "legacy.example",
1415 )?;
1416 let mut archival = Archival::new(fs)?;
1417 assert_eq!(
1418 archival.site.manifest.site_url.as_deref(),
1419 Some("legacy.example")
1420 );
1421 assert_eq!(
1422 archival.site.manifest.object_definition_file,
1423 Path::new(LEGACY_OBJECT_DEFINITION_FILE_NAME).to_path_buf()
1424 );
1425 archival.modify_manifest(|m| m.site_url = Some("legacy-edited.example".to_string()))?;
1428 archival.fs_mutex.with_fs(|fs| {
1429 assert!(!fs.exists(Path::new(MANIFEST_FILE_NAME))?);
1430 let written = fs
1431 .read_to_string(Path::new(LEGACY_MANIFEST_FILE_NAME))?
1432 .unwrap_or_default();
1433 assert!(
1434 written.contains("site_url = \"legacy-edited.example\""),
1435 "{}",
1436 written
1437 );
1438 assert!(!written.contains("object_file"), "{}", written);
1441 Ok(())
1442 })?;
1443 Ok(())
1444 }
1445
1446 #[test]
1447 fn canonical_file_names_take_precedence() -> Result<()> {
1448 let mut fs = named_site_fs(
1449 MANIFEST_FILE_NAME,
1450 OBJECT_DEFINITION_FILE_NAME,
1451 "canonical.example",
1452 )?;
1453 fs.write_str(
1454 LEGACY_MANIFEST_FILE_NAME,
1455 "upload_prefix = \"\"\nsite_url = \"legacy.example\"\n".to_string(),
1456 )?;
1457 fs.write_str(
1458 LEGACY_OBJECT_DEFINITION_FILE_NAME,
1459 "[legacy_only]\nname = \"string\"\n".to_string(),
1460 )?;
1461 let mut archival = Archival::new(fs)?;
1462 assert_eq!(
1463 archival.site.manifest.site_url.as_deref(),
1464 Some("canonical.example")
1465 );
1466 assert!(archival.site.object_definitions.contains_key("site"));
1467 assert!(!archival.site.object_definitions.contains_key("legacy_only"));
1468 archival.modify_manifest(|m| m.site_url = Some("canonical-edited.example".to_string()))?;
1469 archival.fs_mutex.with_fs(|fs| {
1470 let legacy = fs
1471 .read_to_string(Path::new(LEGACY_MANIFEST_FILE_NAME))?
1472 .unwrap_or_default();
1473 assert!(!legacy.contains("canonical-edited"), "{}", legacy);
1474 let canonical = fs
1475 .read_to_string(Path::new(MANIFEST_FILE_NAME))?
1476 .unwrap_or_default();
1477 assert!(
1478 canonical.contains("site_url = \"canonical-edited.example\""),
1479 "{}",
1480 canonical
1481 );
1482 Ok(())
1483 })?;
1484 Ok(())
1485 }
1486
1487 #[test]
1488 fn an_explicit_object_file_is_used_as_written() -> Result<()> {
1489 let mut fs = named_site_fs(
1490 MANIFEST_FILE_NAME,
1491 LEGACY_OBJECT_DEFINITION_FILE_NAME,
1492 "explicit.example",
1493 )?;
1494 fs.write_str(
1495 MANIFEST_FILE_NAME,
1496 "upload_prefix = \"\"\nobject_file = \"custom_objects.toml\"\n".to_string(),
1497 )?;
1498 fs.write_str(
1499 "custom_objects.toml",
1500 "[custom]\nname = \"string\"\n".to_string(),
1501 )?;
1502 let archival = Archival::new(fs)?;
1503 assert_eq!(
1504 archival.site.manifest.object_definition_file,
1505 Path::new("custom_objects.toml").to_path_buf()
1506 );
1507 assert!(archival.site.object_definitions.contains_key("custom"));
1508 let written = archival.site.manifest.to_toml()?;
1509 assert!(
1510 written.contains("object_file = \"custom_objects.toml\""),
1511 "{}",
1512 written
1513 );
1514 Ok(())
1515 }
1516}
1517
1518#[cfg(test)]
1519#[cfg(feature = "typescript")]
1520mod typescript_definitions {
1521 use typescript_type_def::{write_definition_file, DefinitionFileOptions};
1522 use value_path::ValuePath;
1523
1524 use crate::{
1525 fields::FieldType,
1526 object::{RenderedObject, RenderedObjectEntry},
1527 };
1528
1529 use super::*;
1530
1531 #[test]
1532 fn run() {
1533 let mut buf = Vec::new();
1534 let options = DefinitionFileOptions {
1535 header: Some("// AUTO-GENERATED by typescript-type-def\n"),
1536 root_namespace: None,
1537 };
1538 type ExportedTypes = (
1539 ArchivalEvent,
1540 ObjectDefinition,
1541 ValuePath,
1542 FieldType,
1543 RenderedObject,
1544 RenderedObjectEntry,
1545 );
1546 write_definition_file::<_, ExportedTypes>(&mut buf, options).unwrap();
1547 }
1548}