repos
/ analytics-rust master

analytics-rust

mirror archived upstream

Single-binary self-hosted website analytics on Rust axum: collector API, dashboards, world map, and PDF reports.

analyticsaxumdockerrustself-hostedsqliteviteweb-analytics

4.4 KB · 149 lines · Rust Raw History
  1use std::path::{Path, PathBuf};
  2use std::sync::Arc;
  3
  4use chrono::{Datelike, Local};
  5use typst::{
  6    diag::{FileError, FileResult, SourceDiagnostic},
  7    foundations::{Bytes, Datetime},
  8    layout::PagedDocument,
  9    syntax::{FileId, Source, VirtualPath},
 10    text::{Font, FontBook},
 11    utils::LazyHash,
 12    Library, LibraryExt, World,
 13};
 14use typst_kit::fonts::{FontSearcher, FontSlot, Fonts};
 15
 16/// Pre-built renderer state. Fonts and the standard library are loaded once at
 17/// startup and shared across renders.
 18pub struct PdfRenderer {
 19    library: Arc<LazyHash<Library>>,
 20    book: Arc<LazyHash<FontBook>>,
 21    fonts: Arc<Vec<FontSlot>>,
 22    root: PathBuf,
 23}
 24
 25impl PdfRenderer {
 26    /// Discover system + embedded fonts and build the renderer. `root` is the
 27    /// project root that absolute paths in the Typst source resolve against
 28    /// (e.g. `image("/templates/foo.svg")` -> `<root>/templates/foo.svg`).
 29    pub fn new(root: PathBuf) -> Self {
 30        let Fonts { book, fonts } = FontSearcher::new()
 31            .include_system_fonts(true)
 32            .search();
 33        Self {
 34            library: Arc::new(LazyHash::new(Library::default())),
 35            book: Arc::new(LazyHash::new(book)),
 36            fonts: Arc::new(fonts),
 37            root,
 38        }
 39    }
 40
 41    /// Compile `source` (Typst markup) into a PDF.
 42    pub fn render(&self, source: String) -> anyhow::Result<Vec<u8>> {
 43        let main_id = FileId::new(None, VirtualPath::new("/main.typ"));
 44        let main = Source::new(main_id, source);
 45        let world = PdfWorld {
 46            library: self.library.clone(),
 47            book: self.book.clone(),
 48            fonts: self.fonts.clone(),
 49            root: self.root.clone(),
 50            main,
 51        };
 52        let warned = typst::compile::<PagedDocument>(&world);
 53        let document = warned
 54            .output
 55            .map_err(|errs| format_diagnostics("compile", &errs))?;
 56        let bytes = typst_pdf::pdf(&document, &typst_pdf::PdfOptions::default())
 57            .map_err(|errs| format_diagnostics("pdf export", &errs))?;
 58        Ok(bytes)
 59    }
 60}
 61
 62fn format_diagnostics(stage: &str, errs: &[SourceDiagnostic]) -> anyhow::Error {
 63    let mut s = String::new();
 64    for e in errs {
 65        if !s.is_empty() {
 66            s.push('\n');
 67        }
 68        s.push_str(&e.message);
 69        for h in &e.hints {
 70            s.push_str("\n  hint: ");
 71            s.push_str(h);
 72        }
 73    }
 74    anyhow::anyhow!("typst {stage}: {s}")
 75}
 76
 77struct PdfWorld {
 78    library: Arc<LazyHash<Library>>,
 79    book: Arc<LazyHash<FontBook>>,
 80    fonts: Arc<Vec<FontSlot>>,
 81    root: PathBuf,
 82    main: Source,
 83}
 84
 85impl World for PdfWorld {
 86    fn library(&self) -> &LazyHash<Library> {
 87        &self.library
 88    }
 89    fn book(&self) -> &LazyHash<FontBook> {
 90        &self.book
 91    }
 92    fn main(&self) -> FileId {
 93        self.main.id()
 94    }
 95    fn source(&self, id: FileId) -> FileResult<Source> {
 96        if id == self.main.id() {
 97            return Ok(self.main.clone());
 98        }
 99        let path = self.resolve(id)?;
100        let text =
101            std::fs::read_to_string(&path).map_err(|err| FileError::from_io(err, &path))?;
102        Ok(Source::new(id, text))
103    }
104    fn file(&self, id: FileId) -> FileResult<Bytes> {
105        let path = self.resolve(id)?;
106        let bytes = std::fs::read(&path).map_err(|err| FileError::from_io(err, &path))?;
107        Ok(Bytes::new(bytes))
108    }
109    fn font(&self, index: usize) -> Option<Font> {
110        self.fonts.get(index)?.get()
111    }
112    fn today(&self, _offset: Option<i64>) -> Option<Datetime> {
113        let now = Local::now();
114        Datetime::from_ymd(now.year(), now.month() as u8, now.day() as u8)
115    }
116}
117
118impl PdfWorld {
119    fn resolve(&self, id: FileId) -> FileResult<PathBuf> {
120        if id.package().is_some() {
121            return Err(FileError::Other(Some(
122                "remote packages not supported".into(),
123            )));
124        }
125        id.vpath()
126            .resolve(&self.root)
127            .ok_or(FileError::AccessDenied)
128            .and_then(|p| {
129                if path_within(&p, &self.root) {
130                    Ok(p)
131                } else {
132                    Err(FileError::AccessDenied)
133                }
134            })
135    }
136}
137
138fn path_within(path: &Path, root: &Path) -> bool {
139    let canon = match path.canonicalize() {
140        Ok(p) => p,
141        Err(_) => return false,
142    };
143    let canon_root = match root.canonicalize() {
144        Ok(p) => p,
145        Err(_) => return false,
146    };
147    canon.starts_with(canon_root)
148}