1use std::env;
2use std::fs;
3use std::io::{stderr, stdout, Write};
4use std::process::Command;
5
6use chrono::offset::Utc;
7use chrono::DateTime;
8use chrono_humanize::HumanTime;
9use google_drive::GoogleDrive;
10use html2text::from_read;
11use pandoc::OutputKind;
12use regex::Regex;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15
16static QUESTION_TECHNICALLY_CHALLENGING: &str = r"W(?s:.*)at work(?s:.*)ave you found mos(?s:.*)challenging(?s:.*)caree(?s:.*)wh(?s:.*)\?";
18static QUESTION_WORK_PROUD_OF: &str = r"W(?s:.*)at work(?s:.*)ave you done that you(?s:.*)particularl(?s:.*)proud o(?s:.*)and why\?";
19static QUESTION_HAPPIEST_CAREER: &str = r"W(?s:.*)en have you been happiest in your professiona(?s:.*)caree(?s:.*)and why\?";
20static QUESTION_UNHAPPIEST_CAREER: &str = r"W(?s:.*)en have you been unhappiest in your professiona(?s:.*)caree(?s:.*)and why\?";
21static QUESTION_VALUE_REFLECTED: &str = r"F(?s:.*)r one of Oxide(?s:.*)s values(?s:.*)describe an example of ho(?s:.*)it wa(?s:.*)reflected(?s:.*)particula(?s:.*)body(?s:.*)you(?s:.*)work\.";
22static QUESTION_VALUE_VIOLATED: &str = r"F(?s:.*)r one of Oxide(?s:.*)s values(?s:.*)describe an example of ho(?s:.*)it wa(?s:.*)violated(?s:.*)you(?s:.*)organization o(?s:.*)work\.";
23static QUESTION_VALUES_IN_TENSION: &str = r"F(?s:.*)r a pair of Oxide(?s:.*)s values(?s:.*)describe a time in whic(?s:.*)the tw(?s:.*)values(?s:.*)tensio(?s:.*)for(?s:.*)your(?s:.*)and how yo(?s:.*)resolved it\.";
24static QUESTION_WHY_OXIDE: &str = r"W(?s:.*)y do you want to work for Oxide\?";
25
26#[derive(Debug, Default, Deserialize, Serialize)]
29pub struct ApplicantSheetColumns {
30 pub timestamp: usize,
31 pub name: usize,
32 pub email: usize,
33 pub location: usize,
34 pub phone: usize,
35 pub github: usize,
36 pub portfolio: usize,
37 pub website: usize,
38 pub linkedin: usize,
39 pub resume: usize,
40 pub materials: usize,
41 pub status: usize,
42 pub received_application: usize,
43 pub value_reflected: usize,
44 pub value_violated: usize,
45 pub value_in_tension_1: usize,
46 pub value_in_tension_2: usize,
47}
48
49#[derive(Debug, Clone)]
51pub struct Applicant {
52 pub submitted_time: DateTime<Utc>,
53 pub name: String,
54 pub email: String,
55 pub location: String,
56 pub phone: String,
57 pub country_code: String,
58 pub github: String,
59 pub gitlab: String,
60 pub portfolio: String,
61 pub website: String,
62 pub linkedin: String,
63 pub resume: String,
64 pub materials: String,
65 pub status: String,
66 pub received_application: bool,
67 pub role: String,
68 pub sheet_id: String,
69 pub value_reflected: String,
70 pub value_violated: String,
71 pub values_in_tension: Vec<String>,
72}
73
74impl Applicant {
75 pub async fn to_airtable(
76 &self,
77 drive_client: &GoogleDrive,
78 ) -> ApplicantFields {
79 let mut status = "Needs to be triaged";
80
81 if self.status.to_lowercase().contains("next steps") {
82 status = "Next steps";
83 } else if self.status.to_lowercase().contains("deferred") {
84 status = "Deferred";
85 } else if self.status.to_lowercase().contains("declined") {
86 status = "Declined";
87 } else if self.status.to_lowercase().contains("hired") {
88 status = "Hired";
89 }
90
91 let mut location = None;
92 if !self.location.is_empty() {
93 location = Some(self.location.to_string());
94 }
95
96 let mut github = None;
97 if !self.github.is_empty() {
98 github = Some(
99 "https://github.com/".to_owned()
100 + &self.github.replace("@", ""),
101 );
102 }
103
104 let mut linkedin = None;
105 if !self.linkedin.is_empty() {
106 linkedin = Some(self.linkedin.to_string());
107 }
108
109 let mut portfolio = None;
110 if !self.portfolio.is_empty() {
111 portfolio = Some(self.portfolio.to_string());
112 }
113
114 let mut website = None;
115 if !self.website.is_empty() {
116 website = Some(self.website.to_string());
117 }
118
119 let mut value_reflected = None;
120 if !self.value_reflected.is_empty() {
121 value_reflected = Some(self.value_reflected.to_string());
122 }
123
124 let mut value_violated = None;
125 if !self.value_violated.is_empty() {
126 value_violated = Some(self.value_violated.to_string());
127 }
128
129 let mut values_in_tension = None;
130 if !self.values_in_tension.is_empty() {
131 values_in_tension = Some(self.values_in_tension.clone());
132 }
133
134 let rc = get_file_contents(drive_client, self.resume.to_string()).await;
136 let mc =
137 get_file_contents(drive_client, self.materials.to_string()).await;
138
139 let mut resume_contents = None;
140 if !rc.is_empty() {
141 resume_contents = Some(rc);
142 }
143
144 let mut materials_contents = None;
145 if !mc.is_empty() {
146 materials_contents = Some(mc);
147 }
148
149 let mut applicant = ApplicantFields {
150 name: self.name.to_string(),
151 position: self.role.to_string(),
152 status: status.to_string(),
153 timestamp: self.submitted_time,
154 email: self.email.to_string(),
155 phone: self.phone.to_string(),
156 location,
157 github,
158 linkedin,
159 portfolio,
160 website,
161 resume: self.resume.to_string(),
162 materials: self.materials.to_string(),
163 value_reflected,
164 value_violated,
165 values_in_tension,
166 resume_contents,
167 materials_contents,
168 work_samples: None,
169 writing_samples: None,
170 analysis_samples: None,
171 presentation_samples: None,
172 exploratory_samples: None,
173 question_technically_challenging: None,
174 question_proud_of: None,
175 question_happiest: None,
176 question_unhappiest: None,
177 question_value_reflected: None,
178 question_value_violated: None,
179 question_values_in_tension: None,
180 question_why_oxide: None,
181 };
182
183 applicant.parse_materials();
185
186 applicant
187 }
188
189 pub fn human_duration(&self) -> HumanTime {
190 let mut dur = self.submitted_time - Utc::now();
191 if dur.num_seconds() > 0 {
192 dur = -dur;
193 }
194
195 HumanTime::from(dur)
196 }
197
198 pub fn as_slack_msg(&self) -> Value {
199 let mut color = "#805AD5";
200 match self.role.as_str() {
201 "Product Engineering and Design" => color = "#48D597",
202 "Technical Program Management" => color = "#667EEA",
203 _ => (),
204 }
205
206 let time = self.human_duration();
207
208 let mut status_msg = format!("<https://docs.google.com/spreadsheets/d/{}|{}> Applicant | applied {}", self.sheet_id, self.role, time);
209 if !self.status.is_empty() {
210 status_msg += &format!(" | status: *{}*", self.status);
211 }
212
213 let mut values_msg = "".to_string();
214 if !self.value_reflected.is_empty() {
215 values_msg +=
216 &format!("values reflected: *{}*", self.value_reflected);
217 }
218 if !self.value_violated.is_empty() {
219 values_msg += &format!(" | violated: *{}*", self.value_violated);
220 }
221 for (k, tension) in self.values_in_tension.iter().enumerate() {
222 if k == 0 {
223 values_msg += &format!(" | in tension: *{}*", tension);
224 } else {
225 values_msg += &format!(" *& {}*", tension);
226 }
227 }
228 if values_msg.is_empty() {
229 values_msg = "values not yet populated".to_string();
230 }
231
232 let mut intro_msg =
233 format!("*{}* <mailto:{}|{}>", self.name, self.email, self.email,);
234 if !self.location.is_empty() {
235 intro_msg += &format!(" {}", self.location);
236 }
237
238 let mut info_msg = format!(
239 "<{}|resume> | <{}|materials>",
240 self.resume, self.materials,
241 );
242 if !self.phone.is_empty() {
243 info_msg += &format!(" | <tel:{}|{}>", self.phone, self.phone);
244 }
245 if !self.github.is_empty() {
246 info_msg += &format!(
247 " | <https://github.com/{}|github:{}>",
248 self.github.trim_start_matches('@'),
249 self.github,
250 );
251 }
252 if !self.gitlab.is_empty() {
253 info_msg += &format!(
254 " | <https://gitlab.com/{}|gitlab:{}>",
255 self.gitlab.trim_start_matches('@'),
256 self.gitlab,
257 );
258 }
259 if !self.linkedin.is_empty() {
260 info_msg += &format!(" | <{}|linkedin>", self.linkedin,);
261 }
262 if !self.portfolio.is_empty() {
263 info_msg += &format!(" | <{}|portfolio>", self.portfolio,);
264 }
265 if !self.website.is_empty() {
266 info_msg += &format!(" | <{}|website>", self.website,);
267 }
268
269 json!({
270 "response_type": "in_channel",
271 "attachments": [
272 {
273 "color": color,
274 "blocks": [
275 {
276 "type": "section",
277 "text": {
278 "type": "mrkdwn",
279 "text": intro_msg
280 }
281 },
282 {
283 "type": "context",
284 "elements": [
285 {
286 "type": "mrkdwn",
287 "text": info_msg
288 }
289 ]
290 },
291 {
292 "type": "context",
293 "elements": [
294 {
295 "type": "mrkdwn",
296 "text": values_msg
297 }
298 ]
299 },
300 {
301 "type": "context",
302 "elements": [
303 {
304 "type": "mrkdwn",
305 "text": status_msg
306 }
307 ]
308 }
309 ]
310 }
311 ]
312 })
313 }
314
315 pub fn as_company_notification_email(&self) -> String {
316 let time = self.human_duration();
317
318 let mut msg = format!(
319 "## Applicant Information for {}
320
321Submitted {}
322Name: {}
323Email: {}",
324 self.role, time, self.name, self.email
325 );
326
327 if !self.location.is_empty() {
328 msg += &format!("\nLocation: {}", self.location);
329 }
330 if !self.phone.is_empty() {
331 msg += &format!("\nPhone: {}", self.phone);
332 }
333
334 if !self.github.is_empty() {
335 msg += &format!(
336 "\nGitHub: {} (https://github.com/{})",
337 self.github,
338 self.github.trim_start_matches('@')
339 );
340 }
341 if !self.gitlab.is_empty() {
342 msg += &format!(
343 "\nGitLab: {} (https://gitlab.com/{})",
344 self.gitlab,
345 self.gitlab.trim_start_matches('@')
346 );
347 }
348 if !self.linkedin.is_empty() {
349 msg += &format!("\nLinkedIn: {}", self.linkedin);
350 }
351 if !self.portfolio.is_empty() {
352 msg += &format!("\nPortfolio: {}", self.portfolio);
353 }
354 if !self.website.is_empty() {
355 msg += &format!("\nWebsite: {}", self.website);
356 }
357
358 msg+=&format!("\nResume: {}
359Oxide Candidate Materials: {}
360
361## Reminder
362
363To view the all the candidates refer to the following Google spreadsheets:
364
365- Engineering Applications: https://applications-engineering.corp.oxide.computer
366- Product Engineering and Design Applications: https://applications-product.corp.oxide.computer
367- Technical Program Manager Applications: https://applications-tpm.corp.oxide.computer
368",
369 self.resume,
370 self.materials,
371 );
372
373 msg
374 }
375}
376
377#[derive(Debug, Clone, Deserialize, Serialize)]
379pub struct ApplicantFields {
380 #[serde(rename = "Name")]
381 pub name: String,
382 #[serde(rename = "Position")]
383 pub position: String,
384 #[serde(rename = "Status")]
385 pub status: String,
386 #[serde(rename = "Timestamp")]
387 pub timestamp: DateTime<Utc>,
388 #[serde(rename = "Email Address")]
389 pub email: String,
390 #[serde(rename = "Phone Number")]
391 pub phone: String,
392 #[serde(rename = "Location")]
393 pub location: Option<String>,
394 #[serde(rename = "GitHub")]
395 pub github: Option<String>,
396 #[serde(rename = "LinkedIn")]
397 pub linkedin: Option<String>,
398 #[serde(rename = "Portfolio")]
399 pub portfolio: Option<String>,
400 #[serde(rename = "Website")]
401 pub website: Option<String>,
402 #[serde(rename = "Resume")]
403 pub resume: String,
404 #[serde(rename = "Oxide Materials")]
405 pub materials: String,
406 #[serde(rename = "Value Reflected")]
407 pub value_reflected: Option<String>,
408 #[serde(rename = "Value Violated")]
409 pub value_violated: Option<String>,
410 #[serde(rename = "Values in Tension")]
411 pub values_in_tension: Option<Vec<String>>,
412 #[serde(rename = "Resume Contents")]
413 pub resume_contents: Option<String>,
414 #[serde(rename = "Oxide Materials Contents")]
415 pub materials_contents: Option<String>,
416 #[serde(rename = "Work samples")]
417 pub work_samples: Option<String>,
418 #[serde(rename = "Writing samples")]
419 pub writing_samples: Option<String>,
420 #[serde(rename = "Analysis samples")]
421 pub analysis_samples: Option<String>,
422 #[serde(rename = "Presentation samples")]
423 pub presentation_samples: Option<String>,
424 #[serde(rename = "Exploratory samples")]
425 pub exploratory_samples: Option<String>,
426 #[serde(
427 rename = "What work have you found most technically challenging in your career and why?"
428 )]
429 pub question_technically_challenging: Option<String>,
430 #[serde(
431 rename = "What work have you done that you were particularly proud of and why?"
432 )]
433 pub question_proud_of: Option<String>,
434 #[serde(
435 rename = "When have you been happiest in your professional career and why?"
436 )]
437 pub question_happiest: Option<String>,
438 #[serde(
439 rename = "When have you been unhappiest in your professional career and why?"
440 )]
441 pub question_unhappiest: Option<String>,
442 #[serde(
443 rename = "For one of Oxide's values, describe an example of how it was reflected in a particular body of your work."
444 )]
445 pub question_value_reflected: Option<String>,
446 #[serde(
447 rename = "For one of Oxide's values, describe an example of how it was violated in your organization or work."
448 )]
449 pub question_value_violated: Option<String>,
450 #[serde(
451 rename = "For a pair of Oxide's values, describe a time in which the two values came into tension for you or your work, and how you resolved it."
452 )]
453 pub question_values_in_tension: Option<String>,
454 #[serde(rename = "Why do you want to work for Oxide?")]
455 pub question_why_oxide: Option<String>,
456}
457
458impl PartialEq for ApplicantFields {
459 fn eq(&self, other: &Self) -> bool {
460 self.name == other.name
461 && self.position == other.position
462 && self.status == other.status
463 && self.timestamp == other.timestamp
464 && self.email == other.email
465 && self.phone == other.phone
466 && self.location == other.location
467 && self.github == other.github
468 && self.linkedin == other.linkedin
469 && self.portfolio == other.portfolio
470 && self.website == other.website
471 && self.resume == other.resume
472 && self.materials == other.materials
473 && self.value_reflected == other.value_reflected
474 && self.value_violated == other.value_violated
475 && self.values_in_tension == other.values_in_tension
476 && self.resume_contents == other.resume_contents
477 && self.materials_contents == other.materials_contents
478 && self.work_samples == other.work_samples
479 && self.writing_samples == other.writing_samples
480 && self.analysis_samples == other.analysis_samples
481 && self.presentation_samples == other.presentation_samples
482 && self.exploratory_samples == other.exploratory_samples
483 && self.question_technically_challenging
484 == other.question_technically_challenging
485 && self.question_proud_of == other.question_proud_of
486 && self.question_happiest == other.question_happiest
487 && self.question_unhappiest == other.question_unhappiest
488 && self.question_value_reflected == other.question_value_reflected
489 && self.question_value_violated == other.question_value_violated
490 && self.question_values_in_tension
491 == other.question_values_in_tension
492 && self.question_why_oxide == other.question_why_oxide
493 }
494}
495
496impl ApplicantFields {
497 fn parse_materials(&mut self) {
499 let materials_contents;
500 match &self.materials_contents {
501 Some(m) => materials_contents = m,
502 None => return,
503 }
504
505 let mut work_samples = parse_question(
506 r"Work sample\(s\)",
507 "Writing samples",
508 materials_contents,
509 );
510 if work_samples == None {
511 work_samples = parse_question(
512 r"If(?s:.*)his work is entirely proprietary(?s:.*)please describe it as fully as y(?s:.*)can, providing necessary context\.",
513 "Writing samples",
514 materials_contents,
515 );
516 if work_samples == None {
517 work_samples = parse_question(
519 r"What would you have done differently\?",
520 "Exploratory samples",
521 materials_contents,
522 );
523
524 if work_samples == None {
525 work_samples = parse_question(
526 r"Some questions(?s:.*)o have in mind as you describe them:",
527 "Exploratory samples",
528 materials_contents,
529 );
530
531 if work_samples == None {
532 work_samples = parse_question(
533 r"Work samples",
534 "Exploratory samples",
535 materials_contents,
536 );
537 }
538 }
539 }
540 }
541 self.work_samples = work_samples;
542
543 let mut writing_samples = parse_question(
544 r"Writing sample\(s\)",
545 "Analysis samples",
546 materials_contents,
547 );
548 if writing_samples == None {
549 writing_samples = parse_question(
550 r"Please submit at least one writing sample \(and no more tha(?s:.*)three\) that you feel represent(?s:.*)you(?s:.*)providin(?s:.*)links if(?s:.*)necessary\.",
551 "Analysis samples",
552 materials_contents,
553 );
554 if writing_samples == None {
555 writing_samples = parse_question(
556 r"Writing samples",
557 "Analysis samples",
558 materials_contents,
559 );
560 }
561 }
562 self.writing_samples = writing_samples;
563
564 let mut analysis_samples = parse_question(
565 r"Analysis sample\(s\)$",
566 "Presentation samples",
567 materials_contents,
568 );
569 if analysis_samples == None {
570 analysis_samples = parse_question(
571 r"please recount a(?s:.*)incident(?s:.*)which you analyzed syste(?s:.*)misbehavior(?s:.*)including as much technical detail as you can recall\.",
572 "Presentation samples",
573 materials_contents,
574 );
575 if analysis_samples == None {
576 analysis_samples = parse_question(
577 r"Analysis samples",
578 "Presentation samples",
579 materials_contents,
580 );
581 }
582 }
583 self.analysis_samples = analysis_samples;
584
585 let mut presentation_samples = parse_question(
586 r"Presentation sample\(s\)",
587 "Questionnaire",
588 materials_contents,
589 );
590 if presentation_samples == None {
591 presentation_samples = parse_question(
592 r"I(?s:.*)you don’t have a publicl(?s:.*)available presentation(?s:.*)pleas(?s:.*)describe a topic on which you have presented in th(?s:.*)past\.",
593 "Questionnaire",
594 materials_contents,
595 );
596 if presentation_samples == None {
597 presentation_samples = parse_question(
598 r"Presentation samples",
599 "Questionnaire",
600 materials_contents,
601 );
602 }
603 }
604 self.presentation_samples = presentation_samples;
605
606 let mut exploratory_samples = parse_question(
607 r"Exploratory sample\(s\)",
608 "Questionnaire",
609 materials_contents,
610 );
611 if exploratory_samples == None {
612 exploratory_samples = parse_question(
613 r"What’s an example o(?s:.*)something that you needed to explore, reverse engineer, decipher or otherwise figure out a(?s:.*)part of a program or project and how did you do it\? Please provide as much detail as you ca(?s:.*)recall\.",
614 "Questionnaire",
615 materials_contents,
616 );
617 if exploratory_samples == None {
618 exploratory_samples = parse_question(
619 r"Exploratory samples",
620 "Questionnaire",
621 materials_contents,
622 );
623 }
624 }
625 self.exploratory_samples = exploratory_samples;
626
627 let question_technically_challenging = parse_question(
628 QUESTION_TECHNICALLY_CHALLENGING,
629 QUESTION_WORK_PROUD_OF,
630 materials_contents,
631 );
632 self.question_technically_challenging =
633 question_technically_challenging;
634
635 let question_proud_of = parse_question(
636 QUESTION_WORK_PROUD_OF,
637 QUESTION_HAPPIEST_CAREER,
638 materials_contents,
639 );
640 self.question_proud_of = question_proud_of;
641
642 let question_happiest = parse_question(
643 QUESTION_HAPPIEST_CAREER,
644 QUESTION_UNHAPPIEST_CAREER,
645 materials_contents,
646 );
647 self.question_happiest = question_happiest;
648
649 let question_unhappiest = parse_question(
650 QUESTION_UNHAPPIEST_CAREER,
651 QUESTION_VALUE_REFLECTED,
652 materials_contents,
653 );
654 self.question_unhappiest = question_unhappiest;
655
656 let question_value_reflected = parse_question(
657 QUESTION_VALUE_REFLECTED,
658 QUESTION_VALUE_VIOLATED,
659 materials_contents,
660 );
661 self.question_value_reflected = question_value_reflected;
662
663 let question_value_violated = parse_question(
664 QUESTION_VALUE_VIOLATED,
665 QUESTION_VALUES_IN_TENSION,
666 materials_contents,
667 );
668 self.question_value_violated = question_value_violated;
669
670 let question_values_in_tension = parse_question(
671 QUESTION_VALUES_IN_TENSION,
672 QUESTION_WHY_OXIDE,
673 materials_contents,
674 );
675 self.question_values_in_tension = question_values_in_tension;
676
677 let question_why_oxide =
678 parse_question(QUESTION_WHY_OXIDE, "", materials_contents);
679 self.question_why_oxide = question_why_oxide;
680 }
681}
682
683fn parse_question(
684 q1: &str,
685 q2: &str,
686 materials_contents: &str,
687) -> Option<String> {
688 let re = Regex::new(&(q1.to_owned() + r"(?s)(.*)" + q2)).unwrap();
689 let result: Option<String> = if let Some(q) =
690 re.captures(materials_contents)
691 {
692 let val = q.get(1).unwrap();
693 let s = val
694 .as_str()
695 .replace("________________", "")
696 .replace("Oxide Candidate Materials: Technical Program Manager", "")
697 .replace("Oxide Candidate Materials", "")
698 .replace("Work sample(s)", "")
699 .trim_start_matches(':')
700 .trim()
701 .to_string();
702
703 if s.is_empty() {
704 return None;
705 }
706
707 Some(s)
708 } else {
709 None
710 };
711
712 result
713}
714
715async fn get_file_contents(drive_client: &GoogleDrive, url: String) -> String {
717 let id = url.replace("https://drive.google.com/open?id=", "");
718
719 let drive_file = drive_client.get_file_by_id(&id).await.unwrap();
721 let mime_type = drive_file.mime_type.unwrap();
722 let name = drive_file.name.unwrap();
723
724 let mut path = env::temp_dir();
725 let mut output = env::temp_dir();
726
727 let mut result: String = Default::default();
728
729 if mime_type == "application/pdf" {
730 let contents = drive_client.download_file_by_id(&id).await.unwrap();
732
733 path.push(format!("{}.pdf", id));
734
735 let mut file = fs::File::create(path.clone()).unwrap();
736 file.write_all(&contents).unwrap();
737
738 output.push(format!("{}.txt", id));
739
740 let cmd_output = Command::new("pdftotext")
742 .args(&[
743 "-enc",
744 "UTF-8",
745 path.to_str().unwrap(),
746 output.to_str().unwrap(),
747 ])
748 .output()
749 .unwrap();
750
751 result = match fs::read_to_string(output.clone()) {
752 Ok(r) => r,
753 Err(e) => {
754 println!(
755 "running pdf2text failed: {} | name: {}, path: {}",
756 e,
757 name,
758 path.to_str().unwrap()
759 );
760 stdout().write_all(&cmd_output.stdout).unwrap();
761 stderr().write_all(&cmd_output.stderr).unwrap();
762
763 "".to_string()
764 }
765 };
766 } else if mime_type == "text/html" {
767 let contents = drive_client.download_file_by_id(&id).await.unwrap();
768
769 result = from_read(&contents[..], 80);
771 } else if mime_type == "application/vnd.google-apps.document" {
772 result = drive_client.get_file_contents_by_id(&id).await.unwrap();
773 } else if name.ends_with(".doc")
774 || name.ends_with(".pptx")
775 || name.ends_with(".jpg")
776 || name.ends_with(".zip")
777 {
779 println!(
780 "unsupported doc format -- mime type: {}, name: {}, path: {}",
781 mime_type,
782 name,
783 path.to_str().unwrap()
784 );
785 } else {
786 let contents = drive_client.download_file_by_id(&id).await.unwrap();
787 path.push(name.to_string());
788
789 let mut file = fs::File::create(path.clone()).unwrap();
790 file.write_all(&contents).unwrap();
791
792 output.push(format!("{}.txt", id));
793
794 let mut pandoc = pandoc::new();
795 pandoc.add_input(&path);
796 pandoc.set_output(OutputKind::File(output.clone()));
797 pandoc.execute().unwrap();
798
799 result = fs::read_to_string(output.clone()).unwrap();
800 }
801
802 for p in vec![path, output] {
804 if p.exists() && !p.is_dir() {
805 fs::remove_file(p).unwrap();
806 }
807 }
808
809 result.trim().to_string()
810}