hydrate_model/data_source/
file_system_path_based.rs

1use crate::edit_context::EditContext;
2use crate::{AssetSourceId, DataSource, PendingFileOperations};
3use crate::{PathNode, PathNodeRoot};
4use hydrate_base::hashing::HashSet;
5use hydrate_data::json_storage::{MetaFile, MetaFileJson};
6use hydrate_data::{
7    AssetId, AssetLocation, AssetName, CanonicalPathReference, DataSetAssetInfo, HashObjectMode,
8    ImportableName, ImporterId, PathReference,
9};
10use hydrate_pipeline::{
11    HydrateProjectConfiguration, ImportJobSourceFile, ImportJobToQueue, ImportLogEvent, ImportType,
12    Importer, ImporterRegistry, LogEventLevel, PipelineResult, RequestedImportable, ScanContext,
13    ScannedImportable,
14};
15use hydrate_schema::{HashMap, SchemaNamedType};
16use std::ffi::OsStr;
17use std::path::{Path, PathBuf};
18use std::sync::Arc;
19use uuid::Uuid;
20
21// New trait design
22// - fn revert_all(...)
23//   - Determine disk state
24//   - Determine memory state
25//   - Delete/Load anything that doesn't match
26// - fn flush_to_storage(...)
27//   - Determine disk state??
28//   - Determine memory state
29//   - Save anything that doesn't match
30// - fn asset_file_state(...) -> Saved, Modified, RuntimeGenerated
31// - fn asset_is_generated(...)?
32// - fn asset_needs_save(...)?
33// - fn asset_scm_state(...) -> Locked, CheckedOut, Writable,
34// - fn has disk changed and we need to reload?
35// -
36//
37// - Should there be tree-based helpers on asset DB? Mainly to accelerate determining what data
38//   source something is in, drawing UI tree, providing a consistent apparent state even when data
39//   is in bad state. Map IDs to paths? Fix duplicates?
40//
41// IDEA: The database should store paths as strings and ID/Path based systems have to deal with
42// conversion to ID if needed? Means renames touch lots of assets in memory.
43
44// Temporary struct used during load_from_storage call
45struct ScannedSourceFile<'a> {
46    meta_file: MetaFile,
47    importer: &'a Arc<dyn Importer>,
48    scanned_importables: Vec<ScannedImportable>,
49}
50
51struct FileMetadata {
52    // size_in_bytes: u64,
53    // last_modified_time: Option<SystemTime>,
54}
55
56impl FileMetadata {
57    pub fn new(_metadata: &std::fs::Metadata) -> Self {
58        FileMetadata {
59            // size_in_bytes: metadata.len(),
60            // last_modified_time: metadata.modified().ok()
61        }
62    }
63
64    // pub fn has_changed(&self, metadata: &std::fs::Metadata) -> bool {
65    //     self.size_in_bytes != metadata.len() || self.last_modified_time != metadata.modified().ok()
66    // }
67}
68
69// Key: PathBuf
70struct SourceFileDiskState {
71    // may be generated or persisted
72    generated_assets: HashSet<AssetId>,
73    persisted_assets: HashSet<AssetId>,
74    //source_file_metadata: FileMetadata,
75    _importer_id: ImporterId,
76    _importables: HashMap<ImportableName, AssetId>,
77}
78
79// Key: AssetId
80struct GeneratedAssetDiskState {
81    source_file_path: PathBuf, // Immutable, don't need to keep state for the asset, just the source file path
82}
83
84// Key: AssetId
85struct PersistedAssetDiskState {
86    asset_file_path: PathBuf,
87    _asset_file_metadata: FileMetadata,
88    object_hash: u64,
89    // modified time? file length?
90    // hash of asset's on-disk state?
91}
92
93enum AssetDiskState {
94    Generated(GeneratedAssetDiskState),
95    Persisted(PersistedAssetDiskState),
96}
97
98impl AssetDiskState {
99    fn is_persisted(&self) -> bool {
100        match self {
101            AssetDiskState::Generated(_) => false,
102            AssetDiskState::Persisted(_) => true,
103        }
104    }
105
106    fn is_generated(&self) -> bool {
107        !self.is_persisted()
108    }
109
110    fn as_generated_asset_disk_state(&self) -> Option<&GeneratedAssetDiskState> {
111        match self {
112            AssetDiskState::Generated(x) => Some(x),
113            AssetDiskState::Persisted(_) => None,
114        }
115    }
116}
117
118pub struct FileSystemPathBasedDataSource {
119    asset_source_id: AssetSourceId,
120    file_system_root_path: PathBuf,
121
122    importer_registry: ImporterRegistry,
123
124    //all_assigned_path_ids: HashMap<PathBuf, AssetId>,
125    source_files_disk_state: HashMap<PathBuf, SourceFileDiskState>,
126    assets_disk_state: HashMap<AssetId, AssetDiskState>,
127
128    path_node_schema: SchemaNamedType,
129    path_node_root_schema: SchemaNamedType,
130}
131
132impl FileSystemPathBasedDataSource {
133    pub fn asset_source_id(&self) -> AssetSourceId {
134        self.asset_source_id
135    }
136
137    pub fn new<RootPathT: Into<PathBuf>>(
138        file_system_root_path: RootPathT,
139        edit_context: &mut EditContext,
140        asset_source_id: AssetSourceId,
141        importer_registry: &ImporterRegistry,
142    ) -> Self {
143        let path_node_schema = edit_context
144            .schema_set()
145            .find_named_type(PathNode::schema_name())
146            .unwrap()
147            .clone();
148        let path_node_root_schema = edit_context
149            .schema_set()
150            .find_named_type(PathNodeRoot::schema_name())
151            .unwrap()
152            .clone();
153
154        let file_system_root_path = file_system_root_path.into();
155        log::info!(
156            "Creating file system asset data source {:?}",
157            file_system_root_path,
158        );
159
160        FileSystemPathBasedDataSource {
161            asset_source_id,
162            file_system_root_path: file_system_root_path.into(),
163            importer_registry: importer_registry.clone(),
164
165            source_files_disk_state: Default::default(),
166            assets_disk_state: Default::default(),
167
168            path_node_schema,
169            path_node_root_schema,
170        }
171    }
172
173    fn is_asset_owned_by_this_data_source(
174        &self,
175        edit_context: &EditContext,
176        asset_id: AssetId,
177    ) -> bool {
178        if edit_context.asset_schema(asset_id).unwrap().fingerprint()
179            == self.path_node_root_schema.fingerprint()
180        {
181            return false;
182        }
183
184        let root_location = edit_context
185            .asset_location_chain(asset_id)
186            .unwrap_or_default()
187            .last()
188            .cloned()
189            .unwrap_or_else(AssetLocation::null);
190        self.is_root_location_owned_by_this_data_source(&root_location)
191    }
192
193    fn is_root_location_owned_by_this_data_source(
194        &self,
195        root_location: &AssetLocation,
196    ) -> bool {
197        root_location.path_node_id().as_uuid() == *self.asset_source_id.uuid()
198    }
199
200    fn path_for_asset(
201        &self,
202        containing_file_path: &Path,
203        asset_id: AssetId,
204        asset_info: &DataSetAssetInfo,
205    ) -> PathBuf {
206        let is_directory = asset_info.schema().fingerprint() == self.path_node_schema.fingerprint();
207        let asset_name = Self::sanitize_asset_name(asset_id, asset_info.asset_name());
208        let file_name = Self::file_name_for_asset(&asset_name, is_directory);
209        let asset_file_path = containing_file_path.join(file_name);
210        asset_file_path
211    }
212
213    fn containing_file_path_for_asset(
214        &self,
215        edit_context: &EditContext,
216        asset_id: AssetId,
217    ) -> PathBuf {
218        let mut location_chain = edit_context
219            .asset_location_chain(asset_id)
220            .unwrap_or_default();
221
222        let mut parent_dir = self.file_system_root_path.clone();
223
224        // Pop the PathNodeRoot off the chain so we don't include it in the file path
225        let path_node_root_id = location_chain.pop();
226
227        // If the PathNodeRoot doesn't match this data source's asset source ID, we're in an unexpected state.
228        // Default to having the asset show as being in the root of the datasource
229        if path_node_root_id
230            != Some(AssetLocation::new(AssetId::from_uuid(
231                *self.asset_source_id.uuid(),
232            )))
233        {
234            return parent_dir;
235        }
236
237        for location in location_chain.iter().rev() {
238            let name = edit_context.asset_name(location.path_node_id()).unwrap();
239            parent_dir.push(name.as_string().unwrap());
240        }
241
242        parent_dir
243    }
244
245    // fn file_name_for_asset(&self, edit_context: &EditContext, asset_id: AssetId) -> PathBuf {
246    //     let asset_name = edit_context.asset_name(asset_id).as_string().cloned().unwrap_or_else(|| asset_id.as_uuid().to_string());
247    //     let is_directory = edit_context.asset_schema(asset_id).unwrap().fingerprint() == self.path_node_schema.fingerprint();
248    //
249    //     assert!(!asset_name.is_empty());
250    //     if is_directory {
251    //         PathBuf::from(asset_name)
252    //     } else {
253    //         PathBuf::from(format!("{}.af", asset_name))
254    //     }
255    // }
256
257    // Pass asset names through sanitize_asset_name to ensure we don't have an empty string
258    fn file_name_for_asset(
259        asset_name: &str,
260        is_directory: bool,
261    ) -> PathBuf {
262        //let asset_name = edit_context.asset_name(asset_id).as_string().cloned().unwrap_or_else(|| asset_id.as_uuid().to_string());
263        //let is_directory = edit_context.asset_schema(asset_id).unwrap().fingerprint() == self.path_node_schema.fingerprint();
264
265        if is_directory {
266            PathBuf::from(asset_name)
267        } else {
268            PathBuf::from(format!("{}.af", asset_name))
269        }
270    }
271
272    fn sanitize_asset_name(
273        asset_id: AssetId,
274        asset_name: &AssetName,
275    ) -> String {
276        asset_name
277            .as_string()
278            .cloned()
279            .unwrap_or_else(|| asset_id.as_uuid().to_string())
280    }
281
282    fn canonicalize_all_path_nodes(
283        &self,
284        edit_context: &mut EditContext,
285    ) -> HashMap<PathBuf, AssetId> {
286        let mut all_paths: HashMap<PathBuf, AssetId> = Default::default();
287
288        // Go through all the assets and come up with a 1:1 mapping of path node ID to path
289        // - Duplicate path nodes: delete all but one, update all references
290        // - Cyclical references: delete the path nodes and place all assets contained in them at the root
291        // - Empty names: use the asset ID
292        for (k, v) in edit_context.assets() {
293            let mut location_chain = edit_context.asset_location_chain(*k).unwrap_or_default();
294            let root_location = location_chain
295                .last()
296                .cloned()
297                .unwrap_or_else(AssetLocation::null);
298            if !self.is_root_location_owned_by_this_data_source(&root_location) {
299                // Skip anything not owned by this data source
300                continue;
301            }
302
303            // The root location is not needed after this point, pop it off
304            location_chain.pop();
305
306            let is_path_node = v.schema().fingerprint() == self.path_node_schema.fingerprint();
307            if !is_path_node {
308                // Skip anything that is not a path node
309                continue;
310            }
311
312            let mut root_dir = self.file_system_root_path.clone();
313            for element in location_chain {
314                let node_name = edit_context.asset_name(element.path_node_id()).unwrap();
315                let sanitized_name = Self::sanitize_asset_name(element.path_node_id(), node_name);
316                root_dir.push(sanitized_name);
317
318                if all_paths.contains_key(&root_dir) {
319                    // dupe found
320                    // we can delete the dupe and find any assets parented to it and redirect them here later
321                } else {
322                    all_paths.insert(root_dir.clone(), element.path_node_id());
323                }
324            }
325        }
326
327        all_paths.insert(
328            self.file_system_root_path.clone(),
329            AssetId::from_uuid(*self.asset_source_id.uuid()),
330        );
331
332        all_paths
333    }
334
335    fn ensure_asset_location_exists(
336        &self,
337        ancestor_path: &Path,
338        path_to_path_node_id: &mut HashMap<PathBuf, AssetId>,
339        edit_context: &mut EditContext,
340    ) -> AssetLocation {
341        //
342        // Iterate backwards from the file on disk to the root of this data source.
343        // Build the paths that need to exist. We will iterate this list in reverse
344        // to ensure the entire chain of path nodes exist, creating any that are missing.
345        //
346        let mut ancestor_paths = Vec::default();
347        let mut ancestor_path_iter = Some(ancestor_path);
348        let mut found_root = false;
349        while let Some(path) = ancestor_path_iter {
350            if path == self.file_system_root_path {
351                found_root = true;
352                break;
353            }
354
355            ancestor_paths.push(path.to_path_buf());
356            //ancestor_path = path.to_path_buf();
357            ancestor_path_iter = path.parent();
358        }
359
360        // Make sure that when we crawled up the file tree, we terminated at the root of this data source
361        assert!(found_root);
362
363        // If we create a missing path node, we will have to parent it to the previous path node. So
364        // keep track of the previous asset's ID
365        let mut previous_asset_id = AssetId::from_uuid(*self.asset_source_id.uuid());
366
367        // Now traverse the list of ancestors in REVERSE (root -> file)
368        for ancestor_path in ancestor_paths.iter().rev() {
369            if let Some(existing_path_node_id) = path_to_path_node_id.get(ancestor_path) {
370                // The path node already exists, continue
371                previous_asset_id = *existing_path_node_id;
372            } else {
373                // The path node doesn't exist, we need to create it
374                let file_name = ancestor_path.file_name().unwrap().to_string_lossy();
375                let new_path_node_id = edit_context.new_asset(
376                    &AssetName::new(file_name),
377                    &AssetLocation::new(previous_asset_id),
378                    self.path_node_schema.as_record().unwrap(),
379                );
380
381                // add this path node to our canonical list of paths/IDs
382                path_to_path_node_id.insert(ancestor_path.to_path_buf(), new_path_node_id);
383                previous_asset_id = new_path_node_id;
384            }
385        }
386
387        AssetLocation::new(previous_asset_id)
388    }
389
390    fn find_canonical_path_references(
391        project_config: &HydrateProjectConfiguration,
392        source_file_path: &PathBuf,
393        scanned_importable: &ScannedImportable,
394        scanned_source_files: &HashMap<PathBuf, ScannedSourceFile>,
395    ) -> PipelineResult<HashMap<CanonicalPathReference, AssetId>> {
396        // For any referenced file, locate the AssetID at that path. It must be in this data source,
397        // and at this point must exist in the meta file.
398        let mut canonical_path_references = HashMap::default();
399
400        for (path_reference, &importer_id) in &scanned_importable.referenced_source_file_info {
401            let path_reference_absolute =
402                path_reference.canonicalized_absolute_path(project_config, source_file_path)?;
403
404            //println!("referenced {:?} {:?}", path_reference_absolute_path, scanned_source_files.keys());
405            //println!("pull from {:?}", scanned_source_files.keys());
406            //println!("referenced {:?}", path_reference_absolute_path);
407            let referenced_scanned_source_file = scanned_source_files
408                .get(&PathBuf::from(path_reference_absolute.path()))
409                .ok_or_else(|| format!(
410                    "{:?} is referencing source file {:?} via absolute path {:?} but it does not exist or failed to import",
411                    source_file_path,
412                    path_reference.path(),
413                    path_reference_absolute
414                ))?;
415            assert_eq!(
416                importer_id,
417                referenced_scanned_source_file.importer.importer_id()
418            );
419            canonical_path_references.insert(
420                path_reference.clone(),
421                *referenced_scanned_source_file
422                    .meta_file
423                    .past_id_assignments
424                    .get(path_reference.importable_name())
425                    .ok_or_else(|| format!(
426                        "{:?} is referencing importable {:?} in {:?} but it was not found when the file was scanned",
427                        source_file_path,
428                        path_reference.path(),
429                        path_reference.importable_name())
430                    )
431                    .unwrap()
432            );
433        }
434        Ok(canonical_path_references)
435    }
436}
437
438impl DataSource for FileSystemPathBasedDataSource {
439    fn is_generated_asset(
440        &self,
441        asset_id: AssetId,
442    ) -> bool {
443        if let Some(asset_disk_state) = self.assets_disk_state.get(&asset_id) {
444            asset_disk_state.is_generated()
445        } else {
446            false
447        }
448    }
449
450    // fn asset_symbol_name(&self, edit_context: &EditContext, asset_id: AssetId) -> Option<String> {
451    //     //let location_path = edit_context.ro
452    //     None
453    // }
454
455    fn persist_generated_asset(
456        &mut self,
457        edit_context: &mut EditContext,
458        asset_id: AssetId,
459    ) {
460        if !self.is_asset_owned_by_this_data_source(edit_context, asset_id) {
461            return;
462        }
463
464        let old_asset_disk_state = self.assets_disk_state.get(&asset_id).unwrap();
465        if !old_asset_disk_state.is_generated() {
466            return;
467        }
468
469        let old_asset_disk_state = self.assets_disk_state.remove(&asset_id);
470        let source_file_path = old_asset_disk_state
471            .unwrap()
472            .as_generated_asset_disk_state()
473            .unwrap()
474            .source_file_path
475            .clone();
476
477        let mut meta_file_path = source_file_path.clone().into_os_string();
478        meta_file_path.push(".meta");
479
480        //
481        // Write the asset
482        //
483        let containing_file_path = self.containing_file_path_for_asset(edit_context, asset_id);
484        let asset_info = edit_context.assets().get(&asset_id).unwrap();
485        let asset_file_path = self.path_for_asset(&containing_file_path, asset_id, asset_info);
486        // It's a asset, create an asset file
487        let data = crate::json_storage::AssetJson::save_asset_to_string(
488            edit_context.schema_set(),
489            edit_context.assets(),
490            asset_id,
491            true,
492            None,
493        );
494
495        std::fs::create_dir_all(&containing_file_path).unwrap();
496        std::fs::write(&asset_file_path, data).unwrap();
497
498        //
499        // Update the meta file
500        //
501        let contents = std::fs::read_to_string(&meta_file_path).unwrap();
502        let mut meta_file_contents = MetaFileJson::load_from_string(&contents);
503        meta_file_contents.persisted_assets.insert(asset_id);
504        std::fs::write(
505            &meta_file_path,
506            MetaFileJson::store_to_string(&meta_file_contents),
507        )
508        .unwrap();
509
510        //
511        // Update representation of disk state
512        //
513        let object_hash = edit_context
514            .data_set()
515            .hash_object(asset_id, HashObjectMode::FullObjectWithLocationChainNames)
516            .unwrap();
517
518        let asset_file_metadata = FileMetadata::new(&std::fs::metadata(&asset_file_path).unwrap());
519        self.assets_disk_state.insert(
520            asset_id,
521            AssetDiskState::Persisted(PersistedAssetDiskState {
522                _asset_file_metadata: asset_file_metadata,
523                asset_file_path: asset_file_path.clone(),
524                object_hash,
525            }),
526        );
527
528        let source_file_disk_state = self
529            .source_files_disk_state
530            .get_mut(&source_file_path)
531            .unwrap();
532        source_file_disk_state.generated_assets.remove(&asset_id);
533        source_file_disk_state.persisted_assets.insert(asset_id);
534    }
535
536    fn load_from_storage(
537        &mut self,
538        project_config: &HydrateProjectConfiguration,
539        edit_context: &mut EditContext,
540        import_job_to_queue: &mut ImportJobToQueue,
541    ) {
542        profiling::scope!(&format!(
543            "load_from_storage {:?}",
544            self.file_system_root_path
545        ));
546        //
547        // Delete all assets from the database owned by this data source
548        //
549        let mut assets_to_delete = Vec::default();
550        for (asset_id, _) in edit_context.assets() {
551            if self.is_asset_owned_by_this_data_source(edit_context, *asset_id) {
552                assets_to_delete.push(*asset_id);
553            }
554        }
555
556        for asset_to_delete in assets_to_delete {
557            edit_context.delete_asset(asset_to_delete).unwrap();
558        }
559
560        // for (asset_id, asset_disk_state) in &self.assets_disk_state {
561        //     edit_context.delete_asset(*asset_id);
562        // }
563
564        let mut path_to_path_node_id = self.canonicalize_all_path_nodes(edit_context);
565
566        let mut source_files = Vec::default();
567        let mut asset_files = Vec::default();
568        let mut meta_files = Vec::default();
569
570        let mut source_files_disk_state = HashMap::<PathBuf, SourceFileDiskState>::default();
571        let mut assets_disk_state = HashMap::<AssetId, AssetDiskState>::default();
572
573        {
574            profiling::scope!("Categorize files on disk");
575            //
576            // First visit all folders to create path nodes
577            //
578            let walker =
579                globwalk::GlobWalkerBuilder::from_patterns(&self.file_system_root_path, &["**"])
580                    .file_type(globwalk::FileType::DIR)
581                    .build()
582                    .unwrap();
583
584            for file in walker {
585                if let Ok(file) = file {
586                    let asset_file = dunce::canonicalize(&file.path()).unwrap();
587                    let asset_location = self.ensure_asset_location_exists(
588                        &asset_file,
589                        &mut path_to_path_node_id,
590                        edit_context,
591                    );
592                    let asset_id = asset_location.path_node_id();
593
594                    let asset_file_metadata =
595                        FileMetadata::new(&std::fs::metadata(&asset_file).unwrap());
596                    let object_hash = edit_context
597                        .data_set()
598                        .hash_object(asset_id, HashObjectMode::FullObjectWithLocationChainNames)
599                        .unwrap();
600
601                    assets_disk_state.insert(
602                        asset_id,
603                        AssetDiskState::Persisted(PersistedAssetDiskState {
604                            asset_file_path: asset_file,
605                            _asset_file_metadata: asset_file_metadata,
606                            object_hash,
607                        }),
608                    );
609                }
610            }
611
612            //
613            // Visit all files and categorize them as meta files, asset files, or source files
614            // - Asset files end in .af
615            // - Meta files end in .meta
616            // - Anything else is presumed to be a source file
617            //
618            let walker =
619                globwalk::GlobWalkerBuilder::from_patterns(&self.file_system_root_path, &["**"])
620                    .file_type(globwalk::FileType::FILE)
621                    .build()
622                    .unwrap();
623
624            for file in walker {
625                if let Ok(file) = file {
626                    let file = dunce::canonicalize(&file.path()).unwrap();
627                    if file.extension() == Some(OsStr::new("meta")) {
628                        meta_files.push(file.to_path_buf());
629                    } else if file.extension() == Some(OsStr::new("af")) {
630                        asset_files.push(file.to_path_buf());
631                    } else {
632                        source_files.push(file.to_path_buf());
633                    }
634                }
635            }
636        }
637
638        //
639        // Scan all meta files, any asset file that exists and is referenced by a meta file will
640        // be re-imported. (Because the original source asset is presumed to exist alongside the
641        // meta file and source files in a path-based data source get re-imported automatically)
642        //
643        let mut source_file_meta_files = HashMap::<PathBuf, MetaFile>::default();
644        {
645            profiling::scope!("Read meta files");
646            for meta_file in meta_files {
647                let source_file = meta_file.with_extension("");
648                if !source_file.exists() {
649                    println!("Could not find source file, can't re-import data. Restore the source file or delete the meta file.");
650                    continue;
651                }
652                //println!("meta file {:?} source file {:?}", meta_file, source_file);
653
654                let contents = std::fs::read_to_string(meta_file.as_path()).unwrap();
655                let meta_file_contents = MetaFileJson::load_from_string(&contents);
656
657                source_file_meta_files.insert(source_file, meta_file_contents);
658            }
659        }
660
661        //
662        // Load any asset files.
663        //
664        {
665            profiling::scope!("Load Asset Files");
666            for asset_file in asset_files {
667                //println!("asset file {:?}", asset_file);
668                let contents = std::fs::read_to_string(asset_file.as_path()).unwrap();
669
670                let asset_location = self.ensure_asset_location_exists(
671                    asset_file.as_path().parent().unwrap(),
672                    &mut path_to_path_node_id,
673                    edit_context,
674                );
675                let default_asset_location =
676                    AssetLocation::new(AssetId(*self.asset_source_id.uuid()));
677                let schema_set = edit_context.schema_set().clone();
678                let asset_id = crate::json_storage::AssetJson::load_asset_from_string(
679                    edit_context,
680                    &schema_set,
681                    None,
682                    default_asset_location,
683                    Some(asset_location.clone()),
684                    &contents,
685                )
686                .unwrap();
687
688                let asset_file_metadata =
689                    FileMetadata::new(&std::fs::metadata(&asset_file).unwrap());
690
691                let object_hash = edit_context
692                    .data_set()
693                    .hash_object(asset_id, HashObjectMode::FullObjectWithLocationChainNames)
694                    .unwrap();
695
696                assets_disk_state.insert(
697                    asset_id,
698                    AssetDiskState::Persisted(PersistedAssetDiskState {
699                        asset_file_path: asset_file,
700                        _asset_file_metadata: asset_file_metadata,
701                        object_hash,
702                    }),
703                );
704            }
705        }
706
707        //
708        // Scan all the source files and ensure IDs exist for all importables and build a lookup for
709        // finding source files by path. Currently we only allow referencing the unnamed/"default"
710        // importable by path? Maybe we only support implicit import when a file has a single importable?
711        // Don't think it's impossible to support this but the point of supporting paths is to allow
712        // working with files/workflows we can't control, and these things generally just use a plain path.
713        // For now will go ahead and try to support it.
714        //
715
716        //
717        // Scan all the source files and ensure stable IDs exist for all importables. We do this as
718        // a first pass, and a second pass will actually create the assets and ensure references in
719        // the file are satisfied and pointing to the correct asset
720        //
721        let mut scanned_source_files = HashMap::<PathBuf, ScannedSourceFile>::default();
722
723        {
724            profiling::scope!("Scan Source Files");
725
726            for source_file in source_files {
727                //println!("source file first pass {:?}", source_file);
728                // Does a meta file exist?
729                // - If it does: re-import it, but only create new assets if there is not already an asset file
730                // - If it does not: re-import it and create all new asset files
731
732                let extension = &source_file.extension();
733                if extension.is_none() {
734                    // Can happen for files like .DS_Store
735                    continue;
736                }
737
738                let importers = self
739                    .importer_registry
740                    .importers_for_file_extension(&extension.unwrap().to_string_lossy());
741
742                if importers.is_empty() {
743                    // No importer found
744                } else if importers.len() > 1 {
745                    // Multiple importers found, no way of disambiguating
746                } else {
747                    let importer = self.importer_registry.importer(importers[0]).unwrap();
748
749                    let mut scanned_importables = HashMap::default();
750                    {
751                        profiling::scope!(&format!(
752                            "Importer::scan_file {}",
753                            source_file.to_string_lossy()
754                        ));
755                        let scan_result = importer.scan_file(ScanContext::new(
756                            &source_file,
757                            edit_context.schema_set(),
758                            &self.importer_registry,
759                            project_config,
760                            &mut scanned_importables,
761                            &mut import_job_to_queue.log_data.log_events,
762                        ));
763
764                        match scan_result {
765                            Ok(result) => result,
766                            Err(e) => {
767                                import_job_to_queue
768                                    .log_data
769                                    .log_events
770                                    .push(ImportLogEvent {
771                                        path: source_file.clone(),
772                                        asset_id: None,
773                                        level: LogEventLevel::FatalError,
774                                        message: format!(
775                                            "scan_file returned error: {}",
776                                            e.to_string()
777                                        ),
778                                    });
779
780                                continue;
781                            }
782                        }
783                    };
784
785                    //println!("  find meta file {:?}", source_file);
786                    let mut meta_file = source_file_meta_files
787                        .get(&source_file)
788                        .cloned()
789                        .unwrap_or_default();
790                    for (_, scanned_importable) in &scanned_importables {
791                        // Does it exist in the meta file? If so, we need to reuse the ID
792                        meta_file
793                            .past_id_assignments
794                            .entry(scanned_importable.name.clone())
795                            .or_insert_with(|| AssetId::from_uuid(Uuid::new_v4()));
796                    }
797
798                    let mut meta_file_path = source_file.clone().into_os_string();
799                    meta_file_path.push(".meta");
800
801                    //let source_file_metadata = FileMetadata::new(&std::fs::metadata(&source_file).unwrap());
802
803                    let mut importables = HashMap::<ImportableName, AssetId>::default();
804                    for (_, scanned_importable) in &scanned_importables {
805                        let imporable_asset_id =
806                            meta_file.past_id_assignments.get(&scanned_importable.name);
807                        importables.insert(
808                            scanned_importable.name.clone(),
809                            *imporable_asset_id.unwrap(),
810                        );
811                    }
812
813                    source_files_disk_state.insert(
814                        source_file.clone(),
815                        SourceFileDiskState {
816                            generated_assets: Default::default(),
817                            persisted_assets: Default::default(),
818                            //source_file_metadata,
819                            _importer_id: importer.importer_id(),
820                            _importables: importables,
821                        },
822                    );
823
824                    std::fs::write(meta_file_path, MetaFileJson::store_to_string(&meta_file))
825                        .unwrap();
826                    scanned_source_files.insert(
827                        source_file,
828                        ScannedSourceFile {
829                            meta_file,
830                            importer,
831                            scanned_importables: scanned_importables.into_values().collect(),
832                        },
833                    );
834                }
835            }
836        }
837
838        //
839        // Re-import source files
840        //
841        {
842            profiling::scope!("Enqueue import operations");
843            for (source_file_path, scanned_source_file) in &scanned_source_files {
844                let parent_dir = source_file_path.parent().unwrap();
845                //println!("  import to dir {:?}", parent_dir);
846                let import_location =
847                    AssetLocation::new(*path_to_path_node_id.get(parent_dir).unwrap());
848
849                let source_file_disk_state =
850                    source_files_disk_state.get_mut(source_file_path).unwrap();
851
852                let mut requested_importables = HashMap::default();
853                for scanned_importable in &scanned_source_file.scanned_importables {
854                    // The ID assigned to this importable. We have this now because we previously scanned
855                    // all source files and assigned IDs to any importable
856                    let importable_asset_id = *scanned_source_files
857                        .get(source_file_path)
858                        .unwrap()
859                        .meta_file
860                        .past_id_assignments
861                        .get(&scanned_importable.name)
862                        .unwrap();
863
864                    // Create an asset name for this asset
865                    let asset_name =
866                        hydrate_pipeline::create_asset_name(source_file_path, scanned_importable);
867
868                    let asset_file_exists = assets_disk_state.get(&importable_asset_id).is_some();
869                    let asset_is_persisted = scanned_source_file
870                        .meta_file
871                        .persisted_assets
872                        .contains(&importable_asset_id);
873
874                    if asset_is_persisted && !asset_file_exists {
875                        // If the asset is persisted but deleted, we do not want to import it
876                        continue;
877                    }
878
879                    if !asset_is_persisted {
880                        assets_disk_state.insert(
881                            importable_asset_id,
882                            AssetDiskState::Generated(GeneratedAssetDiskState {
883                                source_file_path: source_file_path.clone(),
884                            }),
885                        );
886                        source_file_disk_state
887                            .generated_assets
888                            .insert(importable_asset_id);
889                    } else {
890                        assert!(asset_file_exists);
891                        assert_eq!(
892                            edit_context
893                                .asset_schema(importable_asset_id)
894                                .unwrap()
895                                .fingerprint(),
896                            scanned_importable.asset_type.fingerprint()
897                        );
898                        //edit_context.set_asset_name(importable_asset_id, asset_name);
899                        //edit_context.set_asset_location(importable_asset_id, *import_location);
900                        //edit_context.set_import_info(importable_asset_id, import_info);
901
902                        // We iterated through asset files already, so just check that we inserted a AssetDiskState::Persisted into this map
903                        assert!(assets_disk_state
904                            .get(&importable_asset_id)
905                            .unwrap()
906                            .is_persisted());
907                        source_file_disk_state
908                            .persisted_assets
909                            .insert(importable_asset_id);
910                    }
911
912                    let canonical_path_references = Self::find_canonical_path_references(
913                        project_config,
914                        source_file_path,
915                        &scanned_importable,
916                        &scanned_source_files,
917                    );
918                    match canonical_path_references {
919                        Ok(canonical_path_references) => {
920                            let source_file = PathReference::new(
921                                "".to_string(),
922                                source_file_path.to_string_lossy().to_string(),
923                                scanned_importable.name.clone(),
924                            )
925                            .simplify(project_config);
926
927                            let requested_importable = RequestedImportable {
928                                asset_id: importable_asset_id,
929                                schema: scanned_importable.asset_type.clone(),
930                                asset_name,
931                                asset_location: import_location,
932                                //importer_id: scanned_source_file.importer.importer_id(),
933                                source_file,
934                                canonical_path_references,
935                                path_references: scanned_importable.referenced_source_files.clone(),
936                                replace_with_default_asset: !asset_is_persisted,
937                            };
938
939                            requested_importables
940                                .insert(scanned_importable.name.clone(), requested_importable);
941                        }
942                        Err(e) => {
943                            import_job_to_queue
944                                .log_data
945                                .log_events
946                                .push(ImportLogEvent {
947                                    path: source_file_path.clone(),
948                                    asset_id: Some(importable_asset_id),
949                                    level: LogEventLevel::FatalError,
950                                    message: format!(
951                                        "While resolving references to other assets: {}",
952                                        e.to_string()
953                                    ),
954                                });
955                        }
956                    }
957                }
958
959                if !requested_importables.is_empty() {
960                    import_job_to_queue
961                        .import_job_source_files
962                        .push(ImportJobSourceFile {
963                            source_file_path: source_file_path.to_path_buf(),
964                            importer_id: scanned_source_file.importer.importer_id(),
965                            requested_importables,
966                            import_type: ImportType::ImportIfImportDataStale,
967                        });
968                }
969            }
970        }
971
972        self.assets_disk_state = assets_disk_state;
973        self.source_files_disk_state = source_files_disk_state;
974
975        // //
976        // // Import the file
977        // // - Reuse existing assets if they are referenced by the meta file
978        // // - Create new assets if they do not exist
979        // //
980
981        //
982        // Validate that the rules for supporting loose source files in path-based data sources are being upheld
983        //
984        //
985        //  - When source files are located in a path-based data source:
986        //    - They always get re-scanned and re-imported every time the data source is opened
987        //    - They cannot reference any files via path that are not also in that data source
988        //    - Their assets cannot be renamed or moved. (Users must rename/move the source file)
989        //    - Other assets cannot be stored in a location associated with the source file.
990        //    - When importables are removed from a source file, the asset is not loaded and
991        //      it may break asset references?
992    }
993
994    fn flush_to_storage(
995        &mut self,
996        edit_context: &mut EditContext,
997    ) {
998        profiling::scope!(&format!(
999            "flush_to_storage {:?}",
1000            self.file_system_root_path
1001        ));
1002
1003        let mut pending_writes = Vec::<AssetId>::default();
1004        let mut pending_deletes = Vec::<AssetId>::default();
1005
1006        for &asset_id in edit_context.assets().keys() {
1007            if asset_id.as_uuid() == *self.asset_source_id.uuid() {
1008                // ignore the root asset
1009                continue;
1010            }
1011
1012            if self.is_asset_owned_by_this_data_source(edit_context, asset_id) {
1013                match self.assets_disk_state.get(&asset_id) {
1014                    None => {
1015                        // There is a newly created asset that has never been saved
1016                        pending_writes.push(asset_id);
1017                    }
1018                    Some(asset_disk_state) => {
1019                        let object_hash = edit_context
1020                            .data_set()
1021                            .hash_object(asset_id, HashObjectMode::FullObjectWithLocationChainNames)
1022                            .unwrap();
1023                        match asset_disk_state {
1024                            AssetDiskState::Generated(_) => {
1025                                // We never consider a generated asset as modified, and we expect UI to never alter
1026                                // the asset data
1027                            }
1028                            AssetDiskState::Persisted(persisted_asset_disk_state) => {
1029                                if persisted_asset_disk_state.object_hash != object_hash {
1030                                    // The object has been modified and no longer matches disk state
1031                                    pending_writes.push(asset_id);
1032                                }
1033                            }
1034                        }
1035                    }
1036                }
1037            }
1038        }
1039
1040        // Is there anything that's been deleted?
1041        for (&asset_id, asset_disk_state) in &self.assets_disk_state {
1042            match asset_disk_state {
1043                AssetDiskState::Generated(_) => {
1044                    // We never consider a generated asset as modified, and we expect UI to never alter
1045                    // the asset data
1046                }
1047                AssetDiskState::Persisted(_) => {
1048                    if !edit_context.has_asset(asset_id)
1049                        || !self.is_asset_owned_by_this_data_source(edit_context, asset_id)
1050                    {
1051                        // There is an asset that no longer exists, but the file is still on disk
1052                        pending_deletes.push(asset_id);
1053                    }
1054                }
1055            }
1056        }
1057
1058        // Delete files for assets that were deleted
1059        // for asset_id in edit_context.modified_assets() {
1060        //     if self.all_asset_ids_on_disk_with_original_path.contains_key(asset_id)
1061        //         && !edit_context.has_asset(*asset_id)
1062        //     {
1063        //         //TODO: delete the asset file
1064        //         self.all_asset_ids_on_disk_with_original_path.remove(asset_id);
1065        //     }
1066        // }
1067
1068        //let modified_assets = self.find_all_modified_assets(edit_context);
1069
1070        // We will write out any files that were modified or moved
1071        for asset_id in &pending_writes {
1072            if let Some(asset_info) = edit_context.assets().get(asset_id) {
1073                if self.is_asset_owned_by_this_data_source(edit_context, *asset_id) {
1074                    if asset_id.as_uuid() == *self.asset_source_id.uuid() {
1075                        // never save the root asset
1076                        continue;
1077                    }
1078
1079                    if let Some(asset_disk_state) = self.assets_disk_state.get(asset_id) {
1080                        if asset_disk_state.is_generated() {
1081                            // Never store generated assets, they exist because their source file is
1082                            // on disk and they aren't mutable in the editor
1083                            continue;
1084                        }
1085                    }
1086
1087                    let containing_file_path =
1088                        self.containing_file_path_for_asset(edit_context, *asset_id);
1089                    let is_directory =
1090                        asset_info.schema().fingerprint() == self.path_node_schema.fingerprint();
1091                    let asset_file_path =
1092                        self.path_for_asset(&containing_file_path, *asset_id, asset_info);
1093
1094                    if is_directory {
1095                        // It's a path node, ensure the dir exists
1096                        std::fs::create_dir_all(&asset_file_path).unwrap();
1097                    } else {
1098                        // It's a asset, create an asset file
1099                        let data = crate::json_storage::AssetJson::save_asset_to_string(
1100                            edit_context.schema_set(),
1101                            edit_context.assets(),
1102                            *asset_id,
1103                            true,
1104                            None,
1105                        );
1106
1107                        std::fs::create_dir_all(&containing_file_path).unwrap();
1108                        std::fs::write(&asset_file_path, data).unwrap();
1109
1110                        let object_hash = edit_context
1111                            .data_set()
1112                            .hash_object(
1113                                *asset_id,
1114                                HashObjectMode::FullObjectWithLocationChainNames,
1115                            )
1116                            .unwrap();
1117
1118                        let asset_file_metadata =
1119                            FileMetadata::new(&std::fs::metadata(&asset_file_path).unwrap());
1120                        self.assets_disk_state.insert(
1121                            *asset_id,
1122                            AssetDiskState::Persisted(PersistedAssetDiskState {
1123                                _asset_file_metadata: asset_file_metadata,
1124                                asset_file_path: asset_file_path.clone(),
1125                                object_hash,
1126                            }),
1127                        );
1128
1129                        // We know the asset was already persisted so we don't need to update source files state
1130                    }
1131                }
1132            }
1133        }
1134
1135        let mut deferred_directory_deletes = Vec::default();
1136
1137        // First pass to delete files
1138        for &asset_id in &pending_deletes {
1139            match self.assets_disk_state.get(&asset_id) {
1140                None => {
1141                    // Unexpected, assets pending deletion should be on disk. But we don't need to do anything.
1142                    panic!("assets pending deletion should be on disk");
1143                }
1144                Some(disk_state) => {
1145                    match disk_state {
1146                        AssetDiskState::Generated(_) => {
1147                            // Unexpected, generated assets should not be considered modified and so should not
1148                            // be pending deletion.
1149                            panic!("generated assets should not be considered modified and so should not be pending deletion");
1150                        }
1151                        AssetDiskState::Persisted(disk_state) => {
1152                            if disk_state.asset_file_path.is_dir() {
1153                                // Defer directory deletion so that any files that might be in them get deleted first.
1154                                // We can't delete directories that have files in them.
1155                                deferred_directory_deletes
1156                                    .push((asset_id, disk_state.asset_file_path.clone()));
1157                            } else {
1158                                std::fs::remove_file(&disk_state.asset_file_path).unwrap();
1159                                self.assets_disk_state.remove(&asset_id);
1160                            }
1161                        }
1162                    }
1163                }
1164            }
1165        }
1166
1167        // Reverse sort ensures that subdirectories are processed first
1168        deferred_directory_deletes.sort_by(|(_, lhs), (_, rhs)| rhs.cmp(lhs));
1169
1170        // Second pass to delete directories if they are empty and path node does not exist
1171        for (_, directory) in deferred_directory_deletes {
1172            let is_empty = directory.read_dir().unwrap().next().is_none();
1173            if is_empty {
1174                std::fs::remove_dir(&directory).unwrap();
1175            }
1176        }
1177    }
1178
1179    fn edit_context_has_unsaved_changes(
1180        &self,
1181        edit_context: &EditContext,
1182    ) -> bool {
1183        for (&asset_id, asset_info) in edit_context.assets() {
1184            if asset_id.as_uuid() == *self.asset_source_id.uuid() {
1185                // ignore the root asset
1186                continue;
1187            }
1188
1189            if self.is_asset_owned_by_this_data_source(edit_context, asset_id) {
1190                match self.assets_disk_state.get(&asset_id) {
1191                    None => {
1192                        // There is a newly created asset that has never been saved
1193                        println!("asset name: {:?}", asset_info.asset_name());
1194                        return true;
1195                    }
1196                    Some(asset_disk_state) => {
1197                        let object_hash = edit_context
1198                            .data_set()
1199                            .hash_object(asset_id, HashObjectMode::FullObjectWithLocationChainNames)
1200                            .unwrap();
1201                        match asset_disk_state {
1202                            AssetDiskState::Generated(_) => {
1203                                // We never consider a generated asset as modified, and we expect UI to never alter
1204                                // the asset data
1205                            }
1206                            AssetDiskState::Persisted(persisted_asset_disk_state) => {
1207                                if persisted_asset_disk_state.object_hash != object_hash {
1208                                    // The object has been modified and no longer matches disk state
1209                                    return true;
1210                                }
1211                            }
1212                        }
1213                    }
1214                }
1215            }
1216        }
1217
1218        // Is there anything that's been deleted?
1219        for (&asset_id, asset_disk_state) in &self.assets_disk_state {
1220            match asset_disk_state {
1221                AssetDiskState::Generated(_) => {
1222                    // We never consider a generated asset as modified, and we expect UI to never alter
1223                    // the asset data
1224                }
1225                AssetDiskState::Persisted(_) => {
1226                    if !edit_context.has_asset(asset_id)
1227                        || !self.is_asset_owned_by_this_data_source(edit_context, asset_id)
1228                    {
1229                        // There is an asset that no longer exists, but the file is still on disk
1230                        return true;
1231                    }
1232                }
1233            }
1234        }
1235
1236        return false;
1237    }
1238
1239    fn append_pending_file_operations(
1240        &self,
1241        edit_context: &EditContext,
1242        pending_file_operations: &mut PendingFileOperations,
1243    ) {
1244        for (&asset_id, asset_info) in edit_context.assets() {
1245            if asset_id.as_uuid() == *self.asset_source_id.uuid() {
1246                // ignore the root asset
1247                continue;
1248            }
1249
1250            if self.is_asset_owned_by_this_data_source(edit_context, asset_id) {
1251                match self.assets_disk_state.get(&asset_id) {
1252                    None => {
1253                        // There is a newly created asset that has never been saved
1254                        let containing_path =
1255                            self.containing_file_path_for_asset(edit_context, asset_id);
1256                        let asset_path =
1257                            self.path_for_asset(&containing_path, asset_id, asset_info);
1258                        pending_file_operations
1259                            .create_operations
1260                            .push((asset_id, asset_path));
1261                    }
1262                    Some(asset_disk_state) => {
1263                        let object_hash = edit_context
1264                            .data_set()
1265                            .hash_object(asset_id, HashObjectMode::FullObjectWithLocationChainNames)
1266                            .unwrap();
1267                        match asset_disk_state {
1268                            AssetDiskState::Generated(_) => {
1269                                // We never consider a generated asset as modified, and we expect UI to never alter
1270                                // the asset data
1271                            }
1272                            AssetDiskState::Persisted(persisted_asset_disk_state) => {
1273                                if persisted_asset_disk_state.object_hash != object_hash {
1274                                    // The object has been modified and no longer matches disk state
1275                                    pending_file_operations.modify_operations.push((
1276                                        asset_id,
1277                                        persisted_asset_disk_state.asset_file_path.clone(),
1278                                    ));
1279                                }
1280                            }
1281                        }
1282                    }
1283                }
1284            }
1285        }
1286
1287        // Is there anything that's been deleted?
1288        for (&asset_id, asset_disk_state) in &self.assets_disk_state {
1289            match asset_disk_state {
1290                AssetDiskState::Generated(_) => {
1291                    // We never consider a generated asset as modified, and we expect UI to never alter
1292                    // the asset data
1293                }
1294                AssetDiskState::Persisted(persisted_asset_disk_state) => {
1295                    if !edit_context.has_asset(asset_id)
1296                        || !self.is_asset_owned_by_this_data_source(edit_context, asset_id)
1297                    {
1298                        // There is an asset that no longer exists, but the file is still on disk
1299                        pending_file_operations
1300                            .delete_operations
1301                            .push((asset_id, persisted_asset_disk_state.asset_file_path.clone()));
1302                    }
1303                }
1304            }
1305        }
1306    }
1307}