avatars are served at 42, 96 and 192 px, and kept up to 5 MB
Signed-off-by: dawn <90008@klbr.net>
600 unmodified linesExpand all601602603604605606607608609610611612613614615616617618619620621622623624625626860 unmodified linesExpand all148714881489149014911492149314941495149614971498149915001501150215031773 unmodified linesExpand all3277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310622 unmodified linesExpand all393339343935393639373938393939403941296 unmodified linesExpand all423842394240424142424243424442454246424742484249425042514252425342544255331 unmodified linesExpand all458745884589459045914592459345944595459645974598459946004601460246034604460546064607130 unmodified linesExpand all473847394740474147424743474447454746474747484749475047517 unmodified lines475947604761476247634764476547664767476847694770477147722295 unmodified linesExpand all7068706970707071707270737074707570767077707870797080708170827083708470857086600 unmodified linesExpand allsource = "registry+https://github.com/rust-lang/crates.io-index"checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]name = "bytemuck"version = "1.25.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
[[package]]name = "byteorder"version = "1.5.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]name = "byteorder-lite"version = "0.1.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]name = "bytes"version = "1.12.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"860 unmodified linesExpand allsource = "registry+https://github.com/rust-lang/crates.io-index"checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
[[package]]name = "fdeflate"version = "0.3.7"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"dependencies = [ "simd-adler32",]
[[package]]name = "ff"version = "0.13.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"1773 unmodified linesExpand all "icu_properties",]
[[package]]name = "image"version = "0.25.10"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"dependencies = [ "bytemuck", "byteorder-lite", "image-webp", "moxcms", "num-traits", "png", "zune-core", "zune-jpeg",]
[[package]]name = "image-webp"version = "0.2.4"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"dependencies = [ "byteorder-lite", "quick-error",]
[[package]]name = "indexmap"version = "2.14.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"622 unmodified linesExpand all "gix", "gix-pack", "hashbrown 0.17.1", "http", "image", "ipld-core", "jacquard-common", "jacquard-repo", "knot-acl",296 unmodified linesExpand all "uuid",]
[[package]]name = "moxcms"version = "0.8.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"dependencies = [ "num-traits", "pxfm",]
[[package]]name = "multibase"version = "0.9.2"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "8694bb4835f452b0e3bb06dbebb1d6fc5385b6ca1caf2e55fd165c042390ec77"331 unmodified linesExpand allsource = "registry+https://github.com/rust-lang/crates.io-index"checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]name = "png"version = "0.18.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"dependencies = [ "bitflags 2.13.0", "crc32fast", "fdeflate", "flate2", "miniz_oxide",]
[[package]]name = "polling"version = "3.11.0"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"130 unmodified linesExpand all "parking_lot",]
[[package]]name = "pxfm"version = "0.1.30"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
[[package]]name = "quanta"version = "0.12.6"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7"7 unmodified lines "winapi",]
[[package]]name = "quick-error"version = "2.0.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]name = "quinn"version = "0.11.9"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"2295 unmodified linesExpand alldependencies = [ "cc", "pkg-config",]
[[package]]name = "zune-core"version = "0.5.3"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b"
[[package]]name = "zune-jpeg"version = "0.5.15"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"dependencies = [ "zune-core",]47 unmodified lines48495051525354555640 unmodified lines47 unmodified linestracing = "0.1"tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }url = { version = "2", features = ["serde"] }unicode-segmentation = "1"image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] }futures = "0.3"gix = { version = "0.84", default-features = false }memmap2 = "0.9"gix-pack = { version = "0.71", default-features = false }40 unmodified lines1234567891011121314151617181920212223242526272829303132333435363738394041424344454647//! the small webps an avatar is served at, so a page asking for 42 px doesn't load the original
use std::io::Cursor;use std::path::Path;
use anyhow::Context as _;use image::imageops::FilterType;use image::{DynamicImage, ImageFormat, ImageReader, Limits};
use crate::xrpc::social::{AVATAR_SIZES, sized_avatar};
/// past this the image isn't decoded at all, whatever its bytesconst MAX_PIXELS: u64 = 25_000_000;
pub(super) fn has_sized(avatars: &Path, source: &str) -> bool { AVATAR_SIZES .iter() .all(|&size| avatars.join(sized_avatar(source, size)).is_file())}
/// square crops of the image, each size from the next bigger onepub(super) fn write_sized(avatars: &Path, source: &str, bytes: &[u8]) -> anyhow::Result<()> { let reader = || ImageReader::new(Cursor::new(bytes)).with_guessed_format(); let (width, height) = reader()?.into_dimensions()?; anyhow::ensure!( u64::from(width) * u64::from(height) <= MAX_PIXELS, "a {width}x{height} avatar is past {MAX_PIXELS} pixels" ); let mut limits = Limits::default(); limits.max_alloc = Some(MAX_PIXELS * 8); let mut decoder = reader()?; decoder.limits(limits); let mut image = decoder.decode().context("decode the avatar")?; std::fs::create_dir_all(avatars)?; for &size in AVATAR_SIZES.iter().rev() { image = image.resize_to_fill(size, size, FilterType::Lanczos3); let opaque = if image.color().has_alpha() { DynamicImage::ImageRgba8(image.to_rgba8()) } else { DynamicImage::ImageRgb8(image.to_rgb8()) }; let mut webp = Vec::new(); opaque.write_to(&mut Cursor::new(&mut webp), ImageFormat::WebP)?; crate::cas::atomic_write(&avatars.join(sized_avatar(source, size)), &webp)?; } Ok(())}20 unmodified lines212223242525262728293031293233343536117 unmodified linesExpand all154155156157155158159160161162320 unmodified linesExpand all48348448548648748848949049149249349449549649749849950020 unmodified lines52152252352451452552652752852953053153253353452252352453553653753853954054154254354454554654754854955055155255355476 unmodified lines20 unmodified linesuse crate::config::ValidatedConfig;use crate::datastore::{Datastore, schema};use crate::manifest::{GenerationManifest, RepoEntry};use crate::plan::repo_key;use crate::xrpc::social::{ACCOUNTS_FILE, AVATARS_DIR};use crate::xrpc::social::{ACCOUNTS_FILE, AVATAR_ORIGINAL_MAX, AVATARS_DIR};
use super::avatar_sizes::has_sized;
const REFRESH_TIMEOUT: Duration = Duration::from_secs(3);const AVATAR_TIMEOUT: Duration = Duration::from_secs(10);/// a bigger avatar is only kept at its sizes, see `AVATAR_ORIGINAL_MAX`const AVATAR_MAX_BYTES: u64 = 1_000_000;const AVATAR_FETCH_MAX: u64 = 5_000_000;/// tangled's appview writes the profile under its older sh.tangled nameconst TANGLED_PROFILES: [&str; 2] = ["org.tangled.actor.profile", "sh.tangled.actor.profile"];const PROFILE_COLLECTIONS: [&str; 3] = [ TANGLED_PROFILES[0],117 unmodified linesExpand all authors.extend(stored.unverified.keys().cloned()); } } let Ok(pds) = ReqwestHttp::new(HttpLimits { max_response_bytes: AVATAR_MAX_BYTES, max_response_bytes: AVATAR_FETCH_MAX, ..refresh_limits(config) }) else { return; };320 unmodified linesExpand all if let Some((_, ext)) = served .clone() .find(|(_, ext)| avatars.join(format!("{name}.{ext}")).is_file()) { if !has_sized(avatars, &name) && let Ok(bytes) = std::fs::read(avatars.join(format!("{name}.{ext}"))) { sized(avatars, &name, &bytes); } return Some(Some((name, ext.to_string()))); } if has_sized(avatars, &name) { return Some(Some((name, "webp".to_string()))); } let query = [("did", did), ("cid", name.as_str())]; let mut url = xrpc(endpoint, "com.atproto.sync.getBlob", &query)?.url; let mut hops = 0; let response = loop {20 unmodified lines if response.status.is_server_error() { return None; } let bytes = &response.body; let fits = response.status.is_success() && bytes.len() as u64 <= AVATAR_MAX_BYTES; let fits = response.status.is_success() && bytes.len() as u64 <= AVATAR_FETCH_MAX; if !fits || BlobCid::from_digest(Sha256Digest::hash(bytes)) != cid { return Some(None); } let sniffed = knot_record::attachment::sniffed(bytes); let Some((_, ext)) = served.into_iter().find(|(mime, _)| Some(*mime) == sniffed) else { return Some(None); }; let sized = sized(avatars, &name, bytes); if bytes.len() as u64 <= AVATAR_ORIGINAL_MAX { let path = avatars.join(format!("{name}.{ext}")); std::fs::create_dir_all(avatars).ok()?; crate::cas::atomic_write(&path, bytes).ok()?; let path = avatars.join(format!("{name}.{ext}")); std::fs::create_dir_all(avatars).ok()?; crate::cas::atomic_write(&path, bytes).ok()?; } else if !sized { return Some(None); } Some(Some((name, ext.to_string())))}
/// an image that won't decode is still served whole, just not at its sizesfn sized(avatars: &Path, name: &str, bytes: &[u8]) -> bool { super::avatar_sizes::write_sized(avatars, name, bytes) .inspect_err(|error| tracing::warn!(cid = name, "no sized avatars: {error:#}")) .is_ok()}
pub(super) async fn read_descriptions( chain_root: &Path, entries: &[&RepoEntry],) -> anyhow::Result<BTreeMap<String, String>> {76 unmodified lines22 unmodified lines232425262728293031692 unmodified linesExpand all22 unmodified linesconst UNWIND_THRESHOLD: usize = 12;const POPOVER_CHAR_THRESHOLD: usize = 45;
mod actor_profile;mod avatar_sizes;mod handles;pub mod og;mod pulse;#[cfg(test)]692 unmodified linesExpand all