1mod arrays;
54mod report;
55mod rietveld_wire;
56mod tof_multibank_wire;
57mod tof_structural_wire;
58mod tof_wire;
59mod wire;
60
61use std::collections::BTreeMap;
62use std::error::Error;
63use std::fmt::{Display, Formatter};
64use std::fs;
65use std::io::{Read, Write};
66use std::path::{Path, PathBuf};
67use std::sync::atomic::{AtomicU64, Ordering};
68
69use arrays::{ArrayDescriptor, read_npz, sha256_hex, write_npz};
70use phasesmith_model::{DomainError, ProjectRecord};
71use phasesmith_workflows::{
72 RietveldProjectState, StructuralTofMultiBankProjectState, TofLeBailProjectState,
73 TofMultiBankGeometryProjectState,
74};
75use serde::{Deserialize, Serialize};
76
77pub use report::{
78 HistogramSummary, PhaseSummary, ProjectReportSaveOptions, ProjectSummaryReport,
79 project_summary_json, write_project_summary_json, write_project_summary_json_with_options,
80};
81
82pub const PROJECT_FORMAT_VERSION: u32 = 5;
84pub const PROJECT_MANIFEST_NAME: &str = "manifest.json";
86pub const PROJECT_ARRAYS_NAME: &str = "arrays.npz";
88
89const MANIFEST_BACKUP_NAME: &str = ".manifest.json.phasesmith-backup";
90const ARRAYS_BACKUP_NAME: &str = ".arrays.npz.phasesmith-backup";
91
92static TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0);
93
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub struct ProjectReadLimits {
97 pub max_manifest_bytes: u64,
99 pub max_archive_bytes: u64,
101 pub max_arrays: usize,
103 pub max_array_elements: usize,
105 pub max_uncompressed_array_bytes: u64,
107 pub max_histograms: usize,
109 pub max_phases: usize,
111}
112
113impl Default for ProjectReadLimits {
114 fn default() -> Self {
115 Self {
116 max_manifest_bytes: 16 * 1024 * 1024,
117 max_archive_bytes: 256 * 1024 * 1024,
118 max_arrays: 10_000,
119 max_array_elements: 50_000_000,
120 max_uncompressed_array_bytes: 512 * 1024 * 1024,
121 max_histograms: 10_000,
122 max_phases: 10_000,
123 }
124 }
125}
126
127impl ProjectReadLimits {
128 fn validate(self) -> Result<(), PersistenceError> {
129 if self.max_manifest_bytes == 0
130 || self.max_archive_bytes == 0
131 || self.max_arrays == 0
132 || self.max_array_elements == 0
133 || self.max_uncompressed_array_bytes == 0
134 || self.max_histograms == 0
135 || self.max_phases == 0
136 {
137 return Err(PersistenceError::InvalidLimits);
138 }
139 Ok(())
140 }
141}
142
143#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
145pub struct ProjectSaveOptions {
146 pub overwrite: bool,
148}
149
150#[derive(Debug)]
152pub enum PersistenceError {
153 InvalidLimits,
155 LimitExceeded {
157 message: String,
159 },
160 InvalidDestination {
162 message: String,
164 },
165 Io(std::io::Error),
167 Json(serde_json::Error),
169 UnsupportedVersion {
171 version: u32,
173 },
174 InvalidArray {
176 message: String,
178 },
179 InvalidArchive {
181 message: String,
183 },
184 InvalidRecord {
186 message: String,
188 },
189 Domain(DomainError),
191}
192
193impl Display for PersistenceError {
194 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
195 match self {
196 Self::InvalidLimits => formatter.write_str("all project read limits must be positive"),
197 Self::LimitExceeded { message }
198 | Self::InvalidDestination { message }
199 | Self::InvalidArray { message }
200 | Self::InvalidArchive { message }
201 | Self::InvalidRecord { message } => formatter.write_str(message),
202 Self::Io(error) => Display::fmt(error, formatter),
203 Self::Json(error) => Display::fmt(error, formatter),
204 Self::UnsupportedVersion { version } => {
205 write!(formatter, "unsupported native project format {version}")
206 }
207 Self::Domain(error) => Display::fmt(error, formatter),
208 }
209 }
210}
211
212impl Error for PersistenceError {
213 fn source(&self) -> Option<&(dyn Error + 'static)> {
214 match self {
215 Self::Io(error) => Some(error),
216 Self::Json(error) => Some(error),
217 Self::Domain(error) => Some(error),
218 _ => None,
219 }
220 }
221}
222
223impl From<std::io::Error> for PersistenceError {
224 fn from(error: std::io::Error) -> Self {
225 Self::Io(error)
226 }
227}
228
229impl From<serde_json::Error> for PersistenceError {
230 fn from(error: serde_json::Error) -> Self {
231 Self::Json(error)
232 }
233}
234
235#[derive(Debug, Serialize, Deserialize)]
236#[serde(deny_unknown_fields)]
237struct ArchiveRecord {
238 file: String,
239 sha256: String,
240}
241
242#[derive(Debug, Serialize, Deserialize)]
243#[serde(deny_unknown_fields)]
244struct ProjectManifest {
245 format_version: u32,
246 archive: ArchiveRecord,
247 arrays: BTreeMap<String, ArrayDescriptor>,
248 project: wire::WireProject,
249 #[serde(default)]
250 rietveld_analyses: Option<Vec<rietveld_wire::WireRietveldAnalysis>>,
251 #[serde(default)]
252 tof_lebail_analyses: Option<Vec<tof_wire::WireTofLeBailAnalysis>>,
253 #[serde(default)]
254 tof_multibank_geometry_analyses:
255 Option<Vec<tof_multibank_wire::WireTofMultiBankGeometryAnalysis>>,
256 #[serde(default)]
257 structural_tof_multibank_analyses: Option<Vec<tof_structural_wire::WireStructuralTofAnalysis>>,
258}
259
260#[derive(Debug, Deserialize)]
261struct ProjectVersionProbe {
262 format_version: u32,
263}
264
265type LoadedProjectParts = (
266 ProjectRecord,
267 Vec<rietveld_wire::WireRietveldAnalysis>,
268 Vec<tof_wire::WireTofLeBailAnalysis>,
269 Vec<tof_multibank_wire::WireTofMultiBankGeometryAnalysis>,
270 Vec<tof_structural_wire::WireStructuralTofAnalysis>,
271 BTreeMap<String, arrays::ArrayData>,
272);
273
274pub fn save_project(
284 path: impl AsRef<Path>,
285 project: &ProjectRecord,
286 options: ProjectSaveOptions,
287) -> Result<PathBuf, PersistenceError> {
288 project.validate().map_err(PersistenceError::Domain)?;
289 save_project_parts(
290 path.as_ref(),
291 project,
292 Vec::new(),
293 Vec::new(),
294 Vec::new(),
295 Vec::new(),
296 BTreeMap::new(),
297 options,
298 )
299}
300
301pub fn save_rietveld_project(
308 path: impl AsRef<Path>,
309 state: &RietveldProjectState,
310 options: ProjectSaveOptions,
311) -> Result<PathBuf, PersistenceError> {
312 state
313 .validate()
314 .map_err(|error| PersistenceError::InvalidRecord {
315 message: format!("invalid native Rietveld project state: {error}"),
316 })?;
317 save_project_parts(
318 path.as_ref(),
319 &state.project,
320 rietveld_wire::encode_analyses(state),
321 Vec::new(),
322 Vec::new(),
323 Vec::new(),
324 BTreeMap::new(),
325 options,
326 )
327}
328
329pub fn save_tof_lebail_project(
336 path: impl AsRef<Path>,
337 state: &TofLeBailProjectState,
338 options: ProjectSaveOptions,
339) -> Result<PathBuf, PersistenceError> {
340 state
341 .validate()
342 .map_err(|error| PersistenceError::InvalidRecord {
343 message: format!("invalid native TOF project state: {error}"),
344 })?;
345 let (analyses, arrays) = tof_wire::encode_analyses(state)?;
346 save_project_parts(
347 path.as_ref(),
348 &state.project,
349 Vec::new(),
350 analyses,
351 Vec::new(),
352 Vec::new(),
353 arrays,
354 options,
355 )
356}
357
358pub fn save_tof_multibank_geometry_project(
365 path: impl AsRef<Path>,
366 state: &TofMultiBankGeometryProjectState,
367 options: ProjectSaveOptions,
368) -> Result<PathBuf, PersistenceError> {
369 state
370 .validate()
371 .map_err(|error| PersistenceError::InvalidRecord {
372 message: format!("invalid joint TOF project state: {error}"),
373 })?;
374 let (analyses, arrays) = tof_multibank_wire::encode_analyses(state)?;
375 save_project_parts(
376 path.as_ref(),
377 &state.project,
378 Vec::new(),
379 Vec::new(),
380 analyses,
381 Vec::new(),
382 arrays,
383 options,
384 )
385}
386
387pub fn save_structural_tof_multibank_project(
394 path: impl AsRef<Path>,
395 state: &StructuralTofMultiBankProjectState,
396 options: ProjectSaveOptions,
397) -> Result<PathBuf, PersistenceError> {
398 state
399 .validate()
400 .map_err(|error| PersistenceError::InvalidRecord {
401 message: format!("invalid structural TOF project state: {error}"),
402 })?;
403 save_project_parts(
404 path.as_ref(),
405 &state.project,
406 Vec::new(),
407 Vec::new(),
408 Vec::new(),
409 tof_structural_wire::encode_analyses(state),
410 BTreeMap::new(),
411 options,
412 )
413}
414
415#[allow(clippy::too_many_arguments)] fn save_project_parts(
417 path: &Path,
418 project: &ProjectRecord,
419 rietveld_analyses: Vec<rietveld_wire::WireRietveldAnalysis>,
420 tof_lebail_analyses: Vec<tof_wire::WireTofLeBailAnalysis>,
421 tof_multibank_geometry_analyses: Vec<tof_multibank_wire::WireTofMultiBankGeometryAnalysis>,
422 structural_tof_multibank_analyses: Vec<tof_structural_wire::WireStructuralTofAnalysis>,
423 analysis_arrays: BTreeMap<String, arrays::ArrayData>,
424 options: ProjectSaveOptions,
425) -> Result<PathBuf, PersistenceError> {
426 let destination = absolute_path(path)?;
427 if destination.is_dir() {
428 recover_interrupted_save(&destination)?;
429 }
430 validate_destination(&destination, options)?;
431 let (wire_project, mut arrays) = wire::encode_project(project)?;
432 for (name, value) in analysis_arrays {
433 if arrays.insert(name.clone(), value).is_some() {
434 return Err(PersistenceError::InvalidRecord {
435 message: format!("duplicate project/analysis array name {name:?}"),
436 });
437 }
438 }
439 let encoded_archive = write_npz(&arrays)?;
440 let descriptors = arrays
441 .iter()
442 .map(|(name, value)| (name.clone(), value.descriptor()))
443 .collect();
444 let manifest = ProjectManifest {
445 format_version: PROJECT_FORMAT_VERSION,
446 archive: ArchiveRecord {
447 file: PROJECT_ARRAYS_NAME.to_owned(),
448 sha256: sha256_hex(&encoded_archive),
449 },
450 arrays: descriptors,
451 project: wire_project,
452 rietveld_analyses: Some(rietveld_analyses),
453 tof_lebail_analyses: Some(tof_lebail_analyses),
454 tof_multibank_geometry_analyses: Some(tof_multibank_geometry_analyses),
455 structural_tof_multibank_analyses: Some(structural_tof_multibank_analyses),
456 };
457 let mut encoded_manifest = serde_json::to_string_pretty(&manifest)?;
458 encoded_manifest.push('\n');
459
460 let parent = destination
461 .parent()
462 .ok_or_else(|| PersistenceError::InvalidDestination {
463 message: "project destination has no parent directory".to_owned(),
464 })?;
465 fs::create_dir_all(parent)?;
466 let temporary = create_temporary_directory(parent, &destination)?;
467 let write_result: Result<(), PersistenceError> = (|| {
468 write_synced_file(&temporary.join(PROJECT_ARRAYS_NAME), &encoded_archive)?;
469 write_synced_file(
470 &temporary.join(PROJECT_MANIFEST_NAME),
471 encoded_manifest.as_bytes(),
472 )?;
473 fs::create_dir_all(&destination)?;
474 if options.overwrite {
475 backup_owned_file(
476 &destination.join(PROJECT_MANIFEST_NAME),
477 &destination.join(MANIFEST_BACKUP_NAME),
478 )?;
479 backup_owned_file(
480 &destination.join(PROJECT_ARRAYS_NAME),
481 &destination.join(ARRAYS_BACKUP_NAME),
482 )?;
483 }
484 fs::rename(
485 temporary.join(PROJECT_ARRAYS_NAME),
486 destination.join(PROJECT_ARRAYS_NAME),
487 )?;
488 fs::rename(
489 temporary.join(PROJECT_MANIFEST_NAME),
490 destination.join(PROJECT_MANIFEST_NAME),
491 )?;
492 sync_directory(&destination)?;
493 remove_if_exists(&destination.join(MANIFEST_BACKUP_NAME))?;
494 remove_if_exists(&destination.join(ARRAYS_BACKUP_NAME))?;
495 sync_directory(&destination)?;
496 Ok(())
497 })();
498 if write_result.is_err() && destination.is_dir() {
499 let _ = recover_interrupted_save(&destination);
500 }
501 let _ = fs::remove_dir_all(&temporary);
502 write_result?;
503 Ok(destination)
504}
505
506pub fn load_project(
513 path: impl AsRef<Path>,
514 limits: ProjectReadLimits,
515) -> Result<ProjectRecord, PersistenceError> {
516 let (
517 project,
518 rietveld_analyses,
519 tof_analyses,
520 multibank_analyses,
521 structural_analyses,
522 mut arrays,
523 ) = load_project_parts(path.as_ref(), limits)?;
524 let project = tof_structural_wire::decode_state(project, structural_analyses, limits)?.project;
525 let project =
526 tof_multibank_wire::decode_state(project, multibank_analyses, &mut arrays, limits)?.project;
527 let project = tof_wire::decode_state(project, tof_analyses, &mut arrays, limits)?.project;
528 if !arrays.is_empty() {
529 return Err(PersistenceError::InvalidRecord {
530 message: "manifest contains arrays that are not referenced by the project".to_owned(),
531 });
532 }
533 Ok(rietveld_wire::decode_state(project, rietveld_analyses, limits)?.project)
534}
535
536pub fn load_rietveld_project(
545 path: impl AsRef<Path>,
546 limits: ProjectReadLimits,
547) -> Result<RietveldProjectState, PersistenceError> {
548 let (
549 project,
550 rietveld_analyses,
551 tof_analyses,
552 multibank_analyses,
553 structural_analyses,
554 mut arrays,
555 ) = load_project_parts(path.as_ref(), limits)?;
556 let project = tof_structural_wire::decode_state(project, structural_analyses, limits)?.project;
557 let project =
558 tof_multibank_wire::decode_state(project, multibank_analyses, &mut arrays, limits)?.project;
559 let project = tof_wire::decode_state(project, tof_analyses, &mut arrays, limits)?.project;
560 if !arrays.is_empty() {
561 return Err(PersistenceError::InvalidRecord {
562 message: "manifest contains arrays that are not referenced by the project".to_owned(),
563 });
564 }
565 rietveld_wire::decode_state(project, rietveld_analyses, limits)
566}
567
568pub fn load_tof_lebail_project(
577 path: impl AsRef<Path>,
578 limits: ProjectReadLimits,
579) -> Result<TofLeBailProjectState, PersistenceError> {
580 let (
581 project,
582 rietveld_analyses,
583 tof_analyses,
584 multibank_analyses,
585 structural_analyses,
586 mut arrays,
587 ) = load_project_parts(path.as_ref(), limits)?;
588 let project = tof_structural_wire::decode_state(project, structural_analyses, limits)?.project;
589 let project =
590 tof_multibank_wire::decode_state(project, multibank_analyses, &mut arrays, limits)?.project;
591 rietveld_wire::decode_state(project.clone(), rietveld_analyses, limits)?;
592 let state = tof_wire::decode_state(project, tof_analyses, &mut arrays, limits)?;
593 if !arrays.is_empty() {
594 return Err(PersistenceError::InvalidRecord {
595 message: "manifest contains arrays that are not referenced by the project".to_owned(),
596 });
597 }
598 Ok(state)
599}
600
601pub fn load_tof_multibank_geometry_project(
610 path: impl AsRef<Path>,
611 limits: ProjectReadLimits,
612) -> Result<TofMultiBankGeometryProjectState, PersistenceError> {
613 let (
614 project,
615 rietveld_analyses,
616 tof_analyses,
617 multibank_analyses,
618 structural_analyses,
619 mut arrays,
620 ) = load_project_parts(path.as_ref(), limits)?;
621 let project = tof_structural_wire::decode_state(project, structural_analyses, limits)?.project;
622 rietveld_wire::decode_state(project.clone(), rietveld_analyses, limits)?;
623 let project = tof_wire::decode_state(project, tof_analyses, &mut arrays, limits)?.project;
624 let state = tof_multibank_wire::decode_state(project, multibank_analyses, &mut arrays, limits)?;
625 if !arrays.is_empty() {
626 return Err(PersistenceError::InvalidRecord {
627 message: "manifest contains arrays that are not referenced by the project".to_owned(),
628 });
629 }
630 Ok(state)
631}
632
633pub fn load_structural_tof_multibank_project(
642 path: impl AsRef<Path>,
643 limits: ProjectReadLimits,
644) -> Result<StructuralTofMultiBankProjectState, PersistenceError> {
645 let (
646 project,
647 rietveld_analyses,
648 tof_analyses,
649 multibank_analyses,
650 structural_analyses,
651 mut arrays,
652 ) = load_project_parts(path.as_ref(), limits)?;
653 rietveld_wire::decode_state(project.clone(), rietveld_analyses, limits)?;
654 let project = tof_wire::decode_state(project, tof_analyses, &mut arrays, limits)?.project;
655 let project =
656 tof_multibank_wire::decode_state(project, multibank_analyses, &mut arrays, limits)?.project;
657 let state = tof_structural_wire::decode_state(project, structural_analyses, limits)?;
658 if !arrays.is_empty() {
659 return Err(PersistenceError::InvalidRecord {
660 message: "manifest contains arrays that are not referenced by the project".to_owned(),
661 });
662 }
663 Ok(state)
664}
665
666#[allow(clippy::too_many_lines)] fn load_project_parts(
668 path: &Path,
669 limits: ProjectReadLimits,
670) -> Result<LoadedProjectParts, PersistenceError> {
671 limits.validate()?;
672 let source = absolute_path(path)?;
673 if source.is_dir() {
674 recover_interrupted_save(&source)?;
675 }
676 let manifest_path = source.join(PROJECT_MANIFEST_NAME);
677 let archive_path = source.join(PROJECT_ARRAYS_NAME);
678 let manifest_bytes = read_bounded_file(
679 &manifest_path,
680 limits.max_manifest_bytes,
681 "project manifest exceeds max_manifest_bytes",
682 )?;
683 let version: ProjectVersionProbe = serde_json::from_slice(&manifest_bytes)?;
684 if !(1..=PROJECT_FORMAT_VERSION).contains(&version.format_version) {
685 return Err(PersistenceError::UnsupportedVersion {
686 version: version.format_version,
687 });
688 }
689 let manifest: ProjectManifest = serde_json::from_slice(&manifest_bytes)?;
690 let rietveld_analyses = match (manifest.format_version, manifest.rietveld_analyses) {
691 (1, None) => Vec::new(),
692 (1, Some(_)) => {
693 return Err(PersistenceError::InvalidRecord {
694 message: "native project format 1 cannot declare Rietveld analyses".to_owned(),
695 });
696 }
697 (2..=5, Some(analyses)) => analyses,
698 (2..=5, None) => {
699 return Err(PersistenceError::InvalidRecord {
700 message: format!(
701 "native project format {} requires Rietveld analyses",
702 manifest.format_version
703 ),
704 });
705 }
706 _ => unreachable!("format version checked above"),
707 };
708 let tof_analyses = match (manifest.format_version, manifest.tof_lebail_analyses) {
709 (1 | 2, None) => Vec::new(),
710 (1 | 2, Some(_)) => {
711 return Err(PersistenceError::InvalidRecord {
712 message: format!(
713 "native project format {} cannot declare TOF Le Bail analyses",
714 manifest.format_version
715 ),
716 });
717 }
718 (3..=5, Some(analyses)) => analyses,
719 (3..=5, None) => {
720 return Err(PersistenceError::InvalidRecord {
721 message: format!(
722 "native project format {} requires TOF Le Bail analyses",
723 manifest.format_version
724 ),
725 });
726 }
727 _ => unreachable!("format version checked above"),
728 };
729 let multibank_analyses = match (
730 manifest.format_version,
731 manifest.tof_multibank_geometry_analyses,
732 ) {
733 (1..=3, None) => Vec::new(),
734 (1..=3, Some(_)) => {
735 return Err(PersistenceError::InvalidRecord {
736 message: format!(
737 "native project format {} cannot declare joint TOF analyses",
738 manifest.format_version
739 ),
740 });
741 }
742 (4 | 5, Some(analyses)) => analyses,
743 (4 | 5, None) => {
744 return Err(PersistenceError::InvalidRecord {
745 message: format!(
746 "native project format {} requires joint TOF analyses",
747 manifest.format_version
748 ),
749 });
750 }
751 _ => unreachable!("format version checked above"),
752 };
753 let structural_analyses = match (
754 manifest.format_version,
755 manifest.structural_tof_multibank_analyses,
756 ) {
757 (1..=4, None) => Vec::new(),
758 (1..=4, Some(_)) => {
759 return Err(PersistenceError::InvalidRecord {
760 message: format!(
761 "native project format {} cannot declare structural TOF analyses",
762 manifest.format_version
763 ),
764 });
765 }
766 (5, Some(analyses)) => analyses,
767 (5, None) => {
768 return Err(PersistenceError::InvalidRecord {
769 message: "native project format 5 requires structural TOF analyses".to_owned(),
770 });
771 }
772 _ => unreachable!("format version checked above"),
773 };
774 if manifest.format_version < 3 && wire::has_tof_histograms(&manifest.project) {
775 return Err(PersistenceError::InvalidRecord {
776 message: format!(
777 "native project format {} cannot declare TOF histograms",
778 manifest.format_version
779 ),
780 });
781 }
782 if manifest.archive.file != PROJECT_ARRAYS_NAME {
783 return Err(PersistenceError::InvalidArchive {
784 message: "project archive filename is invalid".to_owned(),
785 });
786 }
787 if manifest.arrays.len() > limits.max_arrays {
788 return Err(PersistenceError::LimitExceeded {
789 message: "project manifest exceeds max_arrays".to_owned(),
790 });
791 }
792 let archive_bytes = read_bounded_file(
793 &archive_path,
794 limits.max_archive_bytes,
795 "project archive exceeds max_archive_bytes",
796 )?;
797 if sha256_hex(&archive_bytes) != manifest.archive.sha256 {
798 return Err(PersistenceError::InvalidArchive {
799 message: "project archive SHA-256 mismatch".to_owned(),
800 });
801 }
802 let mut arrays = read_npz(&archive_bytes, &manifest.arrays, limits)?;
803 let project = wire::decode_project_parts(manifest.project, &mut arrays, limits)?;
804 Ok((
805 project,
806 rietveld_analyses,
807 tof_analyses,
808 multibank_analyses,
809 structural_analyses,
810 arrays,
811 ))
812}
813
814fn read_bounded_file(
815 path: &Path,
816 maximum_bytes: u64,
817 limit_message: &str,
818) -> Result<Vec<u8>, PersistenceError> {
819 let mut bytes = Vec::new();
820 fs::File::open(path)?
821 .take(maximum_bytes.saturating_add(1))
822 .read_to_end(&mut bytes)?;
823 if u64::try_from(bytes.len()).map_or(true, |length| length > maximum_bytes) {
824 return Err(PersistenceError::LimitExceeded {
825 message: limit_message.to_owned(),
826 });
827 }
828 Ok(bytes)
829}
830
831fn absolute_path(path: &Path) -> Result<PathBuf, PersistenceError> {
832 if path.is_absolute() {
833 return Ok(path.to_owned());
834 }
835 Ok(std::env::current_dir()?.join(path))
836}
837
838fn validate_destination(
839 destination: &Path,
840 options: ProjectSaveOptions,
841) -> Result<(), PersistenceError> {
842 if destination.exists() && !destination.is_dir() {
843 return Err(PersistenceError::InvalidDestination {
844 message: format!(
845 "project path exists and is not a directory: {}",
846 destination.display()
847 ),
848 });
849 }
850 if destination.exists() && !options.overwrite {
851 return Err(PersistenceError::InvalidDestination {
852 message: format!(
853 "project directory already exists: {}",
854 destination.display()
855 ),
856 });
857 }
858 Ok(())
859}
860
861fn create_temporary_directory(
862 parent: &Path,
863 destination: &Path,
864) -> Result<PathBuf, PersistenceError> {
865 let stem = destination
866 .file_name()
867 .and_then(|value| value.to_str())
868 .unwrap_or("project");
869 for _ in 0..100 {
870 let sequence = TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
871 let candidate = parent.join(format!(".{stem}-{}-{sequence}.tmp", std::process::id()));
872 match fs::create_dir(&candidate) {
873 Ok(()) => return Ok(candidate),
874 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
875 Err(error) => return Err(PersistenceError::Io(error)),
876 }
877 }
878 Err(PersistenceError::InvalidDestination {
879 message: "could not allocate a temporary project directory".to_owned(),
880 })
881}
882
883fn write_synced_file(path: &Path, bytes: &[u8]) -> Result<(), PersistenceError> {
884 let mut file = fs::File::create(path)?;
885 file.write_all(bytes)?;
886 file.sync_all()?;
887 Ok(())
888}
889
890#[cfg(not(windows))]
891fn sync_directory(path: &Path) -> Result<(), PersistenceError> {
892 fs::File::open(path)?.sync_all()?;
893 Ok(())
894}
895
896#[cfg(windows)]
897fn sync_directory(_path: &Path) -> Result<(), PersistenceError> {
898 Ok(())
902}
903
904fn backup_owned_file(source: &Path, backup: &Path) -> Result<(), PersistenceError> {
905 remove_if_exists(backup)?;
906 if source.exists() {
907 fs::rename(source, backup)?;
908 }
909 Ok(())
910}
911
912fn remove_if_exists(path: &Path) -> Result<(), PersistenceError> {
913 match fs::remove_file(path) {
914 Ok(()) => Ok(()),
915 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
916 Err(error) => Err(PersistenceError::Io(error)),
917 }
918}
919
920fn recover_interrupted_save(directory: &Path) -> Result<(), PersistenceError> {
921 let manifest = directory.join(PROJECT_MANIFEST_NAME);
922 let arrays = directory.join(PROJECT_ARRAYS_NAME);
923 let manifest_backup = directory.join(MANIFEST_BACKUP_NAME);
924 let arrays_backup = directory.join(ARRAYS_BACKUP_NAME);
925 let has_manifest_backup = manifest_backup.exists();
926 let has_arrays_backup = arrays_backup.exists();
927 if !has_manifest_backup && !has_arrays_backup {
928 return Ok(());
929 }
930 if manifest.exists() && arrays.exists() {
931 remove_if_exists(&manifest_backup)?;
932 remove_if_exists(&arrays_backup)?;
933 sync_directory(directory)?;
934 return Ok(());
935 }
936 for (current, backup) in [(&arrays, &arrays_backup), (&manifest, &manifest_backup)] {
937 if backup.exists() {
938 remove_if_exists(current)?;
939 fs::rename(backup, current)?;
940 }
941 }
942 sync_directory(directory)?;
943 Ok(())
944}