1use crate::{
2 Archive, ArchiveKind, LocalArchive,
3 formats::{SourceFormat, SourceFormatId},
4 source_files::SourceDir,
5 utils::{errors::ManifestParseError, ignore_source::IgnoreSource, path_ext::RelPath},
6};
7use flams_backend_types::archive_json::{ArchiveDatum, ArchiveIndex, Institution};
8use ftml_uris::{ArchiveId, ArchiveUri, BaseUri, UriWithArchive};
9use std::path::Path;
10
11#[derive(Debug)]
12pub struct RepositoryData {
13 pub uri: ArchiveUri,
14 pub attributes: Vec<(Box<str>, Box<str>)>,
15 pub formats: smallvec::SmallVec<SourceFormatId, 1>,
16 }
20
21#[allow(clippy::too_many_lines)]
22pub fn parse_manifest(path: &Path, id: RelPath) -> Result<Archive, ManifestParseError> {
24 use std::io::BufRead;
25 let Some(top_dir) = path.parent().and_then(Path::parent) else {
26 return Err(ManifestParseError::NoParent);
27 };
28 let out_path = LocalArchive::out_dir_of(top_dir);
29 let reader = std::fs::File::open(path)?;
30 let reader = std::io::BufReader::new(reader);
31 let mut lines = reader.lines();
32
33 let mut source = None;
34 let mut formats = smallvec::SmallVec::<_, 1>::new();
35 let mut url_base: Option<BaseUri> = None;
36 let mut ignore = IgnoreSource::default();
37 let mut attributes: Vec<(Box<str>, Box<str>)> = Vec::new();
38 let mut real_id: Option<ArchiveId> = None;
39 let mut kind = None;
40 loop {
41 let line = match lines.next() {
42 Some(Err(_)) => continue,
43 Some(Ok(l)) => l,
44 _ => break,
45 };
46 let (k, v) = match line.split_once(':') {
47 Some((k, v)) => (k.trim(), v.trim()),
48 _ => continue,
49 };
50 match k {
51 "id" => {
52 if id != *v {
53 return Err(ManifestParseError::IdMismatch(v.to_string()));
54 } else if v.is_empty() {
55 return Err(ManifestParseError::EmptyId);
56 }
57 real_id = Some(
58 v.parse()
59 .map_err(|_| ManifestParseError::InvalidId(v.to_string()))?,
60 );
61 }
62 "format" => {
63 for f in v.split(',') {
64 formats.push(
65 SourceFormat::get(f)
66 .ok_or_else(|| ManifestParseError::UnknownFormat(f.to_string()))?,
67 );
68 }
69 }
70 "url-base" => {
71 url_base = Some(
72 v.parse()
73 .map_err(|e| ManifestParseError::InvalidUrlBase(v.to_string(), e))?,
74 );
75 }
76 "ignore" => {
77 ignore = IgnoreSource::new(v, &top_dir.join("source")); }
79 "source" => source = Some(v.to_string().into_boxed_str()),
80 "kind" => {
81 if let Some(k) = ArchiveKind::get(v) {
82 kind = Some(k);
83 } else {
84 return Err(ManifestParseError::UnknownKind(v.to_string()));
85 }
86 }
87 _ => {
88 attributes.push((k.into(), v.into()));
89 }
90 }
91 }
92 let Some(id) = real_id else {
93 return Err(ManifestParseError::EmptyId);
94 };
95 if formats.is_empty() && !id.is_meta() && kind.is_none() {
96 return Err(ManifestParseError::NoFormatOrKind);
97 }
98 let Some(dom_uri) = url_base else {
99 return Err(ManifestParseError::NoUrlBase);
100 };
101 let uri = dom_uri & id;
102 if let Some(kind) = kind {
106 let data = RepositoryData {
107 uri,
108 attributes,
109 formats,
110 };
113 (kind.make_new)(data, top_dir).map_or_else(
114 |e| Err(ManifestParseError::InvalidKind(kind.name, e)),
115 |r| Ok(Archive::Ext(kind, r)),
116 )
117 } else {
118 Ok(Archive::Local(Box::new(LocalArchive {
119 uri,
120 formats,
122 ignore,
125 out_path,
126 source,
127 file_state: parking_lot::RwLock::new(SourceDir::default()),
129 #[cfg(feature = "git")]
130 is_managed: std::sync::OnceLock::new(),
131 })))
132 }
133}
134
135pub fn read_archive_json(
136 archive: &ArchiveUri,
137 path: &Path,
138 external_url: &str,
139) -> (Box<[Institution]>, Box<[ArchiveIndex]>) {
140 if !path.exists() {
141 return (Vec::new().into(), Vec::new().into());
142 }
143 let reader = match std::fs::File::open(path) {
144 Ok(reader) => reader,
145 Err(e) => {
146 tracing::error!("Could not read index file {}: {e}", path.display());
147 return (Vec::new().into(), Vec::new().into());
148 }
149 };
150 let reader = std::io::BufReader::new(reader);
151 let v = match serde_json::from_reader::<_, Vec<ArchiveDatum>>(reader) {
152 Ok(v) => v,
153 Err(e) => {
154 tracing::error!("Invalid JSON file {}: {e}", path.display());
155 return (Vec::new().into(), Vec::new().into());
156 }
157 };
158 let mut insts = Vec::new();
159 let mut idxs = Vec::new();
160 for d in v {
161 match d {
162 ArchiveDatum::Document(mut d) => {
163 if d.teaser().is_none() {
164 let desc = path.with_file_name("desc.html");
165 if desc.exists()
166 && let Ok(s) = std::fs::read_to_string(desc)
167 {
168 d.set_teaser(s.into_boxed_str());
169 }
170 }
171 match ArchiveIndex::from_kind(d, archive, |i| {
172 format!(
173 "{external_url}/img?a={}&rp=source/{i}",
174 archive.archive_id()
175 )
176 .into_boxed_str()
177 }) {
178 Ok(e) => idxs.push(e),
179 Err(e) => tracing::error!("Error in index file {}: {e:#}", path.display()),
180 }
181 }
182 ArchiveDatum::Institution(i) => insts.push(match i {
183 Institution::University {
184 title,
185 place,
186 country,
187 url,
188 acronym,
189 logo,
190 } => Institution::University {
191 title,
192 place,
193 country,
194 url,
195 acronym,
196 logo: format!(
197 "{external_url}/img?a={}&rp=source/{logo}",
198 archive.archive_id()
199 )
200 .into_boxed_str(),
201 },
202 Institution::School {
203 title,
204 place,
205 country,
206 url,
207 acronym,
208 logo,
209 } => Institution::School {
210 title,
211 place,
212 country,
213 url,
214 acronym,
215 logo: format!(
216 "{external_url}/img?a={}&rp=source/{logo}",
217 archive.archive_id()
218 )
219 .into_boxed_str(),
220 },
221 }),
222 }
223 }
224 (insts.into(), idxs.into())
225}