1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use cargo::GlobalContext;
5use cargo::core::Workspace;
6use cargo::core::resolver::CliFeatures;
7use cargo::ops::{self, CompileOptions, DocOptions, OutputFormat};
8use cargo::util::command_prelude::CompileMode;
9use flate2::read::GzDecoder;
10use fs_extra::dir::{CopyOptions, copy};
11use kellnr_common::original_name::OriginalName;
12use kellnr_common::version::Version;
13use kellnr_db::{DbProvider, DocQueueEntry};
14use kellnr_storage::kellnr_crate_storage::KellnrCrateStorage;
15use tar::Archive;
16use tokio::fs::{create_dir_all, remove_dir_all};
17use tracing::error;
18
19use crate::compute_doc_url;
20use crate::docs_error::DocsError;
21
22pub fn doc_extraction_queue(
23 db: Arc<dyn DbProvider>,
24 cs: Arc<KellnrCrateStorage>,
25 docs_path: PathBuf,
26 path_prefix: String,
27) {
28 tokio::spawn(async move {
29 loop {
30 tokio::time::sleep(std::time::Duration::from_secs(10)).await;
31 if let Err(e) = inner_loop(db.clone(), &cs, &docs_path, &path_prefix).await {
32 error!("Rustdoc generation loop failed: {e}");
33 }
34 }
35 });
36}
37
38async fn inner_loop(
39 db: Arc<dyn DbProvider>,
40 cs: &KellnrCrateStorage,
41 docs_path: &Path,
42 path_prefix: &str,
43) -> Result<(), DocsError> {
44 let entries = db.get_doc_queue().await?;
45
46 for entry in entries {
47 if let Err(e) = extract_docs(&entry, cs, docs_path).await {
48 error!("Failed to extract docs from crate: {e}");
49 } else {
50 if let Err(e) = clean_up(&entry.path).await {
51 error!("Failed to delete temporary rustdoc queue folder: {e}");
52 }
53
54 let version = Version::from_unchecked_str(&entry.version);
55 let docs_link = compute_doc_url(&entry.normalized_name, &version, path_prefix);
56 db.update_docs_link(&entry.normalized_name, &version, &docs_link)
57 .await?;
58 }
59 db.delete_doc_queue(entry.id).await?;
60 }
61
62 Ok(())
63}
64
65async fn extract_docs(
66 doc: &DocQueueEntry,
67 cs: &KellnrCrateStorage,
68 docs_path: &Path,
69) -> Result<(), DocsError> {
70 let orig_name = OriginalName::from_unchecked(doc.normalized_name.to_string());
74 let version = Version::from_unchecked_str(&doc.version);
75 let contents = cs.get(&orig_name, &version).await.ok_or_else(|| {
76 error!("Failed to get crate from storage");
77 DocsError::CrateDoesNotExist(doc.normalized_name.to_string(), doc.version.clone())
78 })?;
79 let tar = GzDecoder::new(std::io::Cursor::new(contents));
80 let mut archive = Archive::new(tar);
81 archive.unpack(&doc.path)?;
82
83 let generated_docs_path = &doc
85 .path
86 .join(format!("{}-{}", doc.normalized_name, doc.version));
87 generate_docs(generated_docs_path)?;
88
89 let from = generated_docs_path.join("target").join("doc");
91 let to = docs_path
92 .join(doc.normalized_name.to_string())
93 .join(&doc.version);
94 copy_dir(&from, &to).await?;
95
96 Ok(())
97}
98
99async fn clean_up(path: &Path) -> Result<(), DocsError> {
100 remove_dir_all(path).await?;
101 Ok(())
102}
103
104async fn copy_dir(from: &Path, to: &Path) -> Result<(), DocsError> {
105 create_dir_all(to).await?;
106 copy(
107 from,
108 to,
109 &CopyOptions {
110 overwrite: true,
111 ..CopyOptions::default()
112 },
113 )?;
114 Ok(())
115}
116
117fn generate_docs(crate_path: impl AsRef<Path>) -> Result<(), DocsError> {
118 let manifest_path = crate_path.as_ref().join("Cargo.toml").canonicalize()?;
119 let ctx = GlobalContext::default().map_err(|e| DocsError::CargoError(e.to_string()))?;
120 let workspace =
121 Workspace::new(&manifest_path, &ctx).map_err(|e| DocsError::CargoError(e.to_string()))?;
122 let compile_opts = CompileOptions {
123 cli_features: CliFeatures::new_all(true),
124 ..CompileOptions::new(
125 &ctx,
126 CompileMode::Doc {
127 deps: false,
128 json: false,
129 },
130 )
131 .map_err(|e| DocsError::CargoError(e.to_string()))?
132 };
133 let options = DocOptions {
134 open_result: false,
135 compile_opts,
136 output_format: OutputFormat::Html,
137 };
138 ops::doc(&workspace, &options).map_err(|e| DocsError::CargoError(e.to_string()))?;
139 Ok(())
140}