1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
use std::sync::Arc;

use super::ChangeSet;
use crate::config::Project;
use crate::ext::anyhow::{Context, Result};
use crate::service::notify::Watched;
use crate::service::site::SourcedSiteFile;
use crate::signal::{Outcome, Product};
use crate::{ext::PathExt, fs, logger::GRAY};
use camino::{Utf8Path, Utf8PathBuf};
use tokio::task::JoinHandle;

pub async fn assets(proj: &Arc<Project>, changes: &ChangeSet, first_sync: bool) -> JoinHandle<Result<Outcome<Product>>> {
    let changes = changes.clone();

    let proj = proj.clone();
    tokio::spawn(async move {
        let Some(assets) = &proj.assets else {
            return Ok(Outcome::Success(Product::None));
        };
        let dest_root = &proj.site.root_dir;

        let change = if first_sync {
            log::trace!("Assets starting full resync");
            resync(&assets.dir, dest_root).await?;
            true
        } else {
            let mut changed = false;
            for watched in changes.asset_iter() {
                log::trace!("Assets processing {watched:?}");
                let change = update_asset(&proj, watched.clone(), &assets.dir, dest_root, &[]).await?;
                changed |= change;
            }
            changed
        };
        if change {
            log::debug!("Assets finished (with changes)");
            Ok(Outcome::Success(Product::Assets))
        } else {
            log::debug!("Assets finished (no changes)");
            Ok(Outcome::Success(Product::None))
        }
    })
}

async fn update_asset(proj: &Project, watched: Watched, src_root: &Utf8Path, dest_root: &Utf8Path, reserved: &[Utf8PathBuf]) -> Result<bool> {
    if let Some(path) = watched.path() {
        if reserved.contains(path) {
            log::warn!("Assets reserved filename for Glory. Please remove {path:?}");
            return Ok(false);
        }
    }
    Ok(match watched {
        Watched::Create(f) => {
            let to = f.rebase(src_root, dest_root)?;
            if f.is_dir() {
                fs::copy_dir_all(f, to).await?;
            } else {
                fs::copy(&f, &to).await?;
            }
            true
        }
        Watched::Remove(f) => {
            let path = f.rebase(src_root, dest_root)?;
            if path.is_dir() {
                fs::remove_dir_all(&path).await.context(format!("remove dir recursively {path:?}"))?;
            } else {
                fs::remove_file(&path).await.context(format!("remove file {path:?}"))?;
            }
            false
        }
        Watched::Rename(from, to) => {
            let from = from.rebase(src_root, dest_root)?;
            let to = to.rebase(src_root, dest_root)?;
            fs::rename(&from, &to).await.context(format!("rename {from:?} to {to:?}"))?;
            true
        }
        Watched::Write(f) => {
            let file = SourcedSiteFile {
                source: f.clone(),
                dest: f.rebase(src_root, dest_root)?,
                site: f.unbase(src_root)?,
            };
            proj.site.updated(&file).await?
        }
        Watched::Rescan => {
            resync(src_root, dest_root).await?;
            true
        }
    })
}

pub fn reserved(src: &Utf8Path) -> Vec<Utf8PathBuf> {
    vec![src.join("index.html"), src.join("pkg")]
}

// pub async fn update(config: &Config) -> Result<()> {
//     if let Some(src) = &config.glory.assets_dir {
//         let dest = DEST.to_canoncial_dir().dot()?;
//         let src = src.to_canoncial_dir().dot()?;

//         resync(&src, &dest)
//             .await
//             .context(format!("Could not synchronize {src:?} with {dest:?}"))?;
//     }
//     Ok(())
// }

async fn resync(src: &Utf8Path, dest: &Utf8Path) -> Result<()> {
    clean_dest(dest).await.context(format!("Cleaning {dest:?}"))?;
    let reserved = reserved(src);
    mirror(src, dest, &reserved).await.context(format!("Mirroring {src:?} -> {dest:?}"))
}

async fn clean_dest(dest: &Utf8Path) -> Result<()> {
    let mut entries = fs::read_dir(dest).await?;
    while let Some(entry) = entries.next_entry().await? {
        let path = entry.path();

        if entry.file_type().await?.is_dir() {
            if entry.file_name() != "pkg" {
                log::debug!("Assets removing folder {}", GRAY.paint(path.to_string_lossy()));
                fs::remove_dir_all(path).await?;
            }
        } else if entry.file_name() != "index.html" {
            log::debug!("Assets removing file {}", GRAY.paint(path.to_string_lossy()));
            fs::remove_file(path).await?;
        }
    }
    Ok(())
}

async fn mirror(src_root: &Utf8Path, dest_root: &Utf8Path, reserved: &[Utf8PathBuf]) -> Result<()> {
    let mut entries = src_root.read_dir_utf8()?;
    while let Some(Ok(entry)) = entries.next() {
        let from = entry.path().to_path_buf();
        let to = from.rebase(src_root, dest_root)?;
        if reserved.contains(&from) {
            log::warn!("");
            continue;
        }

        if entry.file_type()?.is_dir() {
            log::debug!("Assets copy folder {} -> {}", GRAY.paint(from.as_str()), GRAY.paint(to.as_str()));
            fs::copy_dir_all(from, to).await?;
        } else {
            log::debug!("Assets copy file {} -> {}", GRAY.paint(from.as_str()), GRAY.paint(to.as_str()));
            fs::copy(from, to).await?;
        }
    }
    Ok(())
}