1use pagesift_api::types::{Upload, UploadKind};
6
7use super::*;
8
9const DEFAULT_PARSE_TIMEOUT: Duration = Duration::from_secs(30);
12
13impl Scraper {
14 pub async fn parse(&self, upload: Upload, opts: &ScrapeOptions) -> Result<Document, ApiError> {
16 let Some(audit) = &self.audit else {
17 return self.scrape_inner(Source::Upload(upload), opts).await;
18 };
19 let name = format!("upload:{}", upload.filename);
20 let size = upload.bytes.len();
21 let started = Instant::now();
22 let result = self.scrape_inner(Source::Upload(upload), opts).await;
23 audit.record_parse(&name, size, opts, &result, started.elapsed());
24 result
25 }
26
27 pub(super) async fn parse_upload(
29 &self,
30 upload: Upload,
31 opts: &ScrapeOptions,
32 ) -> Result<Document, ApiError> {
33 let started = Instant::now();
34 let deadline = started
35 + opts
36 .timeout
37 .map_or(DEFAULT_PARSE_TIMEOUT, Duration::from_millis);
38 let content_type = match upload.kind {
39 UploadKind::Html => upload
40 .content_type
41 .clone()
42 .filter(|c| c.to_ascii_lowercase().contains("html")),
43 UploadKind::Pdf | UploadKind::Office(_) => None,
44 };
45 let source = upload
47 .url
48 .as_ref()
49 .map_or_else(|| upload.filename.clone(), Url::to_string);
50 let meta = Metadata {
51 source_url: Some(source),
52 status_code: 200,
53 content_type: Some(
54 content_type
55 .clone()
56 .unwrap_or_else(|| upload.kind.mime().into()),
57 ),
58 proxy_used: "basic".into(),
59 ..Default::default()
60 };
61 let ext = PagesiftMeta {
62 engine: Some("upload".into()),
63 ..Default::default()
64 };
65 let progress = Progress {
66 started,
67 timings: BTreeMap::new(),
68 warnings: Vec::new(),
69 };
70 match upload.kind {
71 UploadKind::Pdf => {
72 self.pdf_document(upload.bytes, opts, (meta, ext), progress, Some(deadline))
73 .await
74 }
75 UploadKind::Office(format) => {
76 let input = office::OfficeInput {
77 bytes: upload.bytes,
78 format,
79 content_type: upload.content_type,
80 };
81 self.office_document(input, opts, (meta, ext), progress, Some(deadline))
82 .await
83 }
84 UploadKind::Html => {
85 let html = pagesift_net::decode_owned(upload.bytes, content_type.as_deref(), None);
86 let base = upload.url.unwrap_or_else(|| upload_base(&upload.filename));
87 self.parse_html(html, &base, opts, (meta, ext), progress, deadline)
88 .await
89 }
90 }
91 }
92
93 async fn parse_html(
94 &self,
95 decoded: pagesift_net::Decoded,
96 base: &Url,
97 opts: &ScrapeOptions,
98 (mut meta, mut ext): (Metadata, PagesiftMeta),
99 mut p: Progress,
100 deadline: Instant,
101 ) -> Result<Document, ApiError> {
102 if let Some(warning) = decoded.warning() {
103 p.warnings.push(warning);
104 }
105 let stage = self
106 .policy
107 .fetcher()
108 .acquire_stage(pagesift_net::NetworkStage::Parse)
109 .await;
110 let t = Instant::now();
111 let work = process_html(decoded.text, base, Some(200), opts);
112 let (doc, html) = match tokio::time::timeout_at(deadline.into(), work).await {
113 Ok(Ok(result)) => {
114 stage.finish(pagesift_net::StageOutcome::Success);
115 result
116 }
117 Ok(Err(error)) => {
118 stage.finish(pagesift_net::StageOutcome::DeterministicFailure);
119 return Err(error);
120 }
121 Err(_) => {
122 stage.finish(pagesift_net::StageOutcome::DeterministicFailure);
123 return Err(ApiError::new(
124 ErrorCode::Timeout,
125 "HTML extraction did not finish within the timeout",
126 ));
127 }
128 };
129 p.timings.insert("extract".into(), ms(t.elapsed()));
130 if doc.metadata.video_fallback {
131 p.warnings.push(
132 "video_metadata: no readable body text; used the page's embedded video metadata"
133 .into(),
134 );
135 }
136 if doc.barrier.is_some() {
138 p.warnings.push(
139 "access_barrier: the uploaded HTML looks like a bot-check or access-barrier page, not the content"
140 .into(),
141 );
142 }
143 fill_metadata(&mut meta, &doc.metadata);
144 if meta
146 .favicon
147 .as_deref()
148 .is_some_and(|f| f.starts_with("file:"))
149 {
150 meta.favicon = None;
151 }
152 fill_structured(&mut ext, &doc);
153 fill_fingerprints(&mut ext, &doc);
154 p.warnings.extend(doc.coverage.warnings());
156 let raw_bytes = Some(thin_basis_bytes(&html, &doc.markdown));
157 let video_fallback = doc.metadata.video_fallback;
158 Ok(finish(
159 document(doc, opts, html),
160 meta,
161 ext,
162 p.timings,
163 p.warnings,
164 raw_bytes,
165 video_fallback,
166 p.started,
167 ))
168 }
169}
170
171fn upload_base(filename: &str) -> Url {
174 let mut base = Url::parse("file:///").expect("static URL");
175 base.set_path(filename);
176 base
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn upload_base_resolves_relative_links_locally() {
185 let base = upload_base("my report.html");
186 assert_eq!(base.as_str(), "file:///my%20report.html");
187 assert_eq!(
188 base.join("img/a.png").unwrap().as_str(),
189 "file:///img/a.png"
190 );
191 }
192}