better deriving

This commit is contained in:
tezlm 2023-07-27 02:51:29 -07:00
parent 6e3d2c580b
commit ed80b60e13
Signed by: tezlm
GPG key ID: 649733FCD94AFBBA
10 changed files with 371 additions and 240 deletions

View file

@ -1,198 +0,0 @@
use std::io::Cursor;
use infer::MatcherType;
use serde::Deserialize;
use ufh::derived::{DeriveFile, DeriveMedia};
/// derive basic information from files
pub fn derive_file(buffer: &[u8], name: Option<&str>) -> DeriveFile {
if let Some(mime) = infer::get(buffer) {
let (height, width, duration) = match mime.matcher_type() {
MatcherType::Image => {
match image_dimensions(buffer) {
Ok((width, height)) => (Some(width), Some(height), None),
Err(_) => (None, None, None),
}
},
// maybe use ffmpeg for these
MatcherType::Video => (None, None, None), // TODO: extract video information
MatcherType::Audio => (None, None, None), // TODO: extract audio information
_ => (None, None, None),
};
return DeriveFile {
size: buffer.len() as u64,
mime: mime.to_string(),
height,
width,
duration
}
}
if let Some(name) = name {
if let Some(mime) = mime_guess::from_path(name).first() {
let (height, width, duration) = match mime.type_().into() {
"image" => match image_dimensions(buffer) {
Ok((width, height)) => (Some(width), Some(height), None),
Err(_) => (None, None, None),
},
// maybe use ffmpeg for these
"video" => (None, None, None), // TODO: extract video information
"audio" => (None, None, None), // TODO: extract audio information
_ => (None, None, None),
};
return DeriveFile {
size: buffer.len() as u64,
mime: mime.to_string(),
height,
width,
duration
}
}
}
DeriveFile {
size: buffer.len() as u64,
mime: "application/octet-stream".to_owned(),
height: None,
width: None,
duration: None,
}
}
// TODO: make this code less cancerous
#[derive(Debug, Default, Deserialize)]
struct FFprobeData {
title: Option<String>,
purl: Option<String>,
album: Option<String>,
artist: Option<String>,
comment: Option<String>,
}
#[derive(Debug, Deserialize)]
struct FFprobeDataWrapper (
#[serde(deserialize_with = "serde_aux::container_attributes::deserialize_struct_case_insensitive")] FFprobeData,
);
#[allow(unused)]
/// derive tag information from media, like images/video/audio
pub async fn derive_media(buffer: &[u8]) -> Option<DeriveMedia> {
match infer::get(buffer)?.matcher_type() {
MatcherType::Image | MatcherType::Audio | MatcherType::Video => (),
_ => return None,
};
use tokio::process::Command;
use std::process::{Stdio, ChildStdin};
use std::io::{BufWriter, Cursor};
let mut cmd = Command::new("/usr/bin/ffprobe")
.args(["-v", "quiet", "-of", "json", "-show_streams", "-show_format", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.expect("couldn't find ffprobe");
let mut cmd_stdin = cmd.stdin
.take()
.expect("ffprobe should take stdin");
let mut cursor = Cursor::new(&buffer);
let r = tokio::io::copy(&mut cursor, &mut cmd_stdin).await;
drop(cmd_stdin);
let output = cmd.wait_with_output().await.expect("failed to read").stdout;
let string = String::from_utf8(output).expect("ffprobe should output utf8");
let info: serde_json::Value = serde_json::from_str(&string).expect("ffprobe should output json");
// TODO: find a better way to do this?
use serde_json::Value;
let format_info: FFprobeData = info.get("format")
.and_then(|a| a.get("tags"))
.and_then(|a| serde_json::from_value(a.clone()).ok())
.map(|a: FFprobeDataWrapper| a.0)
.unwrap_or_default();
let stream_info: FFprobeData = info.get("streams")
.and_then(|a| a.get(0))
.and_then(|a| a.get("tags"))
.and_then(|a| serde_json::from_value(a.clone()).ok())
.map(|a: FFprobeDataWrapper| a.0)
.unwrap_or_default();
let media = DeriveMedia {
title: stream_info.title.or(format_info.title),
artist: stream_info.artist.or(format_info.artist),
album: stream_info.album.or(format_info.album),
comment: stream_info.comment.or(format_info.comment),
url: stream_info.purl.or(format_info.purl),
};
if media.is_empty() {
None
} else {
Some(media)
}
}
/// derive/extract text from file formats, for full text search
// in the future, maybe i could have derive_embed with clip for image searching?
pub async fn derive_text(buffer: &[u8], media: Option<&DeriveMedia>, mime: Option<&str>) -> Option<String> {
fn concat(parts: &[Option<&str>]) -> Option<String> {
let mut out = String::new();
for part in parts.iter().flatten() {
out += part;
out += " - ";
}
if out.is_empty() {
None
} else {
out.truncate(out.len() - " - ".len());
Some(out)
}
}
let Some(mime) = mime else {
// TODO: extract text from event?
return None;
};
match mime.split('/').next().unwrap() {
"text" => String::from_utf8(buffer.into()).ok(),
"image" | "video" | "audio" => match media {
Some(media) => concat(&[
media.title.as_deref(),
media.artist.as_deref(),
media.album.as_deref(),
media.url.as_deref(),
media.comment.as_deref(),
]),
None => None,
},
// MatcherType::Image => {}, // ocr
// MatcherType::Video => {}, // fts (subtitles)
// MatcherType::Audio => {}, // fts (lyrics)
// MatcherType::Archive => {}, // filenames, maybe?
// MatcherType::Book => {}, // fts (extract)
// MatcherType::Doc => {}, // fts (extract)
_ => None,
}
}
#[allow(unused)]
/// derive/generate thumbnails from various files
pub fn derive_thumbnail(buffer: &[u8]) -> Vec<u8> {
// MatcherType::Image => {}, // thumbnail itself
// MatcherType::Video => {}, // extract thumbnail, otherwise extract
// MatcherType::Audio => {}, // extract album art
todo!();
}
fn image_dimensions(buffer: &[u8]) -> Result<(u64, u64), image::ImageError> {
let (width, height) = image::io::Reader::new(Cursor::new(buffer))
.with_guessed_format()?
.into_dimensions()?;
Ok((width as u64, height as u64))
}

View file

@ -0,0 +1,23 @@
use tokio::process::Command;
use std::process::Stdio;
use std::io::Cursor;
pub async fn extract(buffer: &[u8], args: &[&str]) -> Result<Vec<u8>, ()> {
let mut cmd = Command::new("/usr/bin/ffmpeg")
.args([&["-v", "quiet", "-i", "-"], args].concat())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.expect("couldn't find ffmpeg");
let mut cmd_stdin = cmd.stdin
.take()
.expect("ffmpeg should take stdin");
let mut cursor = Cursor::new(&buffer);
let _ = tokio::io::copy(&mut cursor, &mut cmd_stdin).await;
drop(cmd_stdin);
Ok(cmd.wait_with_output().await.map_err(|_| ())?.stdout)
}

View file

@ -0,0 +1,122 @@
use serde::{Serialize, Deserialize};
use tokio::process::Command;
use ufh::derived::DeriveMedia;
use std::collections::HashMap;
use std::process::Stdio;
use std::io::Cursor;
use super::ffmpeg;
#[derive(Debug, Serialize, Deserialize)]
pub struct Ffprobe {
pub streams: Vec<Stream>,
pub format: Format,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Stream {
pub index: u64,
pub codec_type: String,
pub height: Option<u64>,
pub width: Option<u64>,
pub duration: Option<String>,
#[serde(default)]
pub tags: Tags,
pub disposition: Disposition,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Format {
pub duration: Option<String>,
#[serde(default)]
pub tags: Tags,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Disposition {
// if it's a thumbnail
pub attached_pic: u8,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Tags(#[serde(deserialize_with = "serde_aux::container_attributes::deserialize_struct_case_insensitive")] HashMap<String, String>);
impl Ffprobe {
pub async fn derive(buffer: &[u8]) -> Ffprobe {
let mut cmd = Command::new("/usr/bin/ffprobe")
.args(["-v", "quiet", "-of", "json", "-show_streams", "-show_format", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.expect("couldn't find ffprobe");
let mut cmd_stdin = cmd.stdin
.take()
.expect("ffprobe should take stdin");
let mut cursor = Cursor::new(&buffer);
let _ = tokio::io::copy(&mut cursor, &mut cmd_stdin).await;
drop(cmd_stdin);
let output = cmd.wait_with_output().await.expect("failed to read").stdout;
let string = String::from_utf8(output).expect("ffprobe should output utf8");
serde_json::from_str(&string).expect("ffprobe should output json")
}
pub fn media_info(&self) -> DeriveMedia {
let mut info = self.format.tags.media_info();
for stream in &self.streams {
let new_info = stream.tags.media_info();
info = DeriveMedia {
title: info.title.or(new_info.title),
album: info.album.or(new_info.album),
artist: info.artist.or(new_info.artist),
comment: info.comment.or(new_info.comment),
url: info.url.or(new_info.url),
};
}
info
}
pub fn dimensions(&self) -> (Option<u64>, Option<u64>, Option<u64>) {
let mut dimensions = (None, None, self.format.duration.clone().and_then(|s| s.parse().ok()));
for stream in &self.streams {
let (width, height, duration) = stream.dimensions();
dimensions.0 = dimensions.0.or(width);
dimensions.1 = dimensions.1.or(height);
dimensions.2 = dimensions.2.or(duration);
}
dimensions
}
pub async fn thumbnail(&self, buffer: &[u8]) -> Option<Vec<u8>> {
match self.streams.iter().find(|s| s.disposition.attached_pic == 1) {
Some(stream) => ffmpeg::extract(buffer, &["-map", &format!("0:{}", stream.index), "-f", "webp", "-"]).await.ok(),
None => ffmpeg::extract(buffer, &["-vframes", "1", "-f", "webp", "-"]).await.ok(),
}
}
pub async fn subtitles(&self, buffer: &[u8]) -> Option<String> {
let stream = self.streams.iter().find(|s| s.codec_type == "subtitle")?;
String::from_utf8(ffmpeg::extract(buffer, &["-map", &format!("0:{}", stream.index), "-f", "webvtt", "-"]).await.ok()?).ok()
}
}
impl Stream {
pub fn dimensions(&self) -> (Option<u64>, Option<u64>, Option<u64>) {
(self.width, self.height, self.duration.clone().and_then(|s| s.parse().ok()))
}
}
impl Tags {
pub fn media_info(&self) -> DeriveMedia {
let map = &self.0;
DeriveMedia {
title: map.get("title").map(String::from),
artist: map.get("artist").map(String::from),
album: map.get("album").map(String::from),
comment: map.get("comment").map(String::from),
url: map.get("purl").map(String::from),
}
}
}

127
server/src/derive/mod.rs Normal file
View file

@ -0,0 +1,127 @@
use std::io::Cursor;
use bytes::Bytes;
use ufh::derived::{DeriveFile, DeriveMedia};
pub mod ffprobe;
pub mod ffmpeg;
pub struct Deriver {
ffprobe: Option<ffprobe::Ffprobe>,
mime: String,
major: String,
buffer: Bytes,
}
impl Deriver {
pub async fn begin(buffer: Bytes, name: Option<&str>) -> Deriver {
let mime = infer::get(&buffer).map(|m| m.to_string())
.or_else(|| name.map(mime_guess::from_path).and_then(|m| m.first()).map(|m| m.to_string()))
.unwrap_or_else(|| "application/octet-stream".into());
let major = mime.clone().split('/').next().unwrap().to_string();
Deriver {
ffprobe: match major.as_str() {
"image" | "video" | "audio" => Some(ffprobe::Ffprobe::derive(&buffer).await),
_ => None,
},
mime,
major,
buffer
}
}
pub async fn get_file(&self) -> DeriveFile {
let (width, height, duration) = match self.major.as_str() {
"image" => match image_dimensions(&self.buffer) {
Ok((width, height)) => (Some(width), Some(height), None),
Err(_) => self.ffprobe.as_ref().unwrap().dimensions(),
},
"video" | "audio" => {
self.ffprobe.as_ref().unwrap().dimensions()
},
_ => (None, None, None),
};
DeriveFile {
size: self.buffer.len() as u64,
mime: self.mime.clone(),
height,
width,
duration,
}
}
/// derive tag information from media, like images/video/audio
pub async fn get_media(&self) -> Option<DeriveMedia> {
match self.major.as_str() {
"image" | "audio" | "video" => (),
_ => return None,
};
let media = self.ffprobe.as_ref().unwrap().media_info();
if media.is_empty() {
None
} else {
Some(media)
}
}
/// derive/extract text from file formats, for full text search
// in the future, maybe i could have derive_embed with clip for image searching?
pub async fn get_text(&self) -> Option<String> {
fn concat(parts: &[Option<&str>]) -> Option<String> {
let mut out = String::new();
for part in parts.iter().flatten() {
out += part;
out += " - ";
}
if out.is_empty() {
None
} else {
out.truncate(out.len() - " - ".len());
Some(out)
}
}
match self.major.as_str() {
"text" => String::from_utf8(self.buffer.to_vec()).ok(),
"image" | "video" | "audio" => {
let ffprobe = self.ffprobe.as_ref().unwrap();
let subs = ffprobe.subtitles(&self.buffer).await;
let media = ffprobe.media_info();
let info = concat(&[
media.title.as_deref(),
media.artist.as_deref(),
media.album.as_deref(),
media.url.as_deref(),
media.comment.as_deref(),
]);
concat(&[info.as_deref(), subs.as_deref()])
},
// MatcherType::Image => {}, // ocr
// MatcherType::Archive => {}, // filenames, maybe?
// MatcherType::Book => {}, // fts (extract)
// MatcherType::Doc => {}, // fts (extract)
_ => None,
}
}
#[allow(unused)]
/// derive/generate thumbnails from various files
pub async fn get_thumbnail(&self) -> Option<Bytes> {
match self.major.as_str() {
"video" | "audio" => {
self.ffprobe.as_ref().unwrap().thumbnail(&self.buffer).await.map(Bytes::from)
},
_ => None,
}
}
}
fn image_dimensions(buffer: &[u8]) -> Result<(u64, u64), image::ImageError> {
let (width, height) = image::io::Reader::new(Cursor::new(buffer))
.with_guessed_format()?
.into_dimensions()?;
Ok((width as u64, height as u64))
}

View file

@ -1,7 +1,8 @@
use std::collections::{HashSet, HashMap};
use bytes::Bytes;
use tracing::debug;
use ufh::{derived::Derived, event::{EventContent, Event, RelInfo, FileEvent}, item::ItemRef};
use crate::{state::{db::{DbItem, sqlite::Sqlite, Database}, search::{Search, Document}}, routes::{things::Error, util::get_blob}, derive::{derive_file, derive_media, derive_text}};
use crate::{state::{db::{DbItem, sqlite::Sqlite, Database}, search::{Search, Document}}, routes::{things::Error, util::get_blob}, derive::Deriver};
use super::Items;
type Relations = HashMap<ItemRef, (Event, RelInfo)>;
@ -117,16 +118,15 @@ pub async fn commit_special(me: &Items, action: &DelayedAction) -> Result<(), Er
Ok(())
}
pub async fn derive(me: &Items, event: &Event, file: &FileEvent) -> Result<Derived, Error> {
pub async fn derive(me: &Items, event: &Event, file: &FileEvent) -> Result<(Derived, Option<Bytes>), Error> {
let bytes = get_blob(me, event, None).await?;
let derived_file = derive_file(&bytes, file.name.as_deref());
let derived_media = derive_media(&bytes).await;
let deriver = Deriver::begin(bytes, file.name.as_deref()).await;
let derived = Derived {
file: Some(derived_file),
media: derived_media,
file: Some(deriver.get_file().await),
media: deriver.get_media().await,
tags: HashSet::new(),
};
Ok(derived)
Ok((derived, deriver.get_thumbnail().await))
}
pub async fn update_search_index(me: &Items, event: &Event, relations: &Relations) -> Result<(), Error> {
@ -144,9 +144,8 @@ pub async fn update_search_index(me: &Items, event: &Event, relations: &Relation
pub async fn reindex(me: &Items, event: &Event) -> Result<(), Error> {
let bytes = get_blob(me, event, None).await?;
let mime = event.derived.file.as_ref().map(|i| i.mime.as_str());
let file = match &event.content { EventContent::File(f) => Some(f), _ => None };
let derived = derive_text(&bytes, event.derived.media.as_ref(), mime).await;
let derived = Deriver::begin(bytes, None).await.get_text().await;
let doc = Document {
text: derived.unwrap_or_default(),
name: file.and_then(|f| f.name.as_deref()),

View file

@ -5,7 +5,7 @@ use once_cell::sync::OnceCell;
use tokio::sync::Mutex;
use tracing::{debug, info};
use ufh::{event::{Event, WipEvent, EventContent}, item::ItemRef, derived::Derived};
use crate::{state::{db::{sqlite::Sqlite, Database}, search::tantivy::Tantivy}, routes::things::Error, blobs, Relations, items::events::update_search_index};
use crate::{state::{db::{sqlite::Sqlite, Database}, search::tantivy::Tantivy}, routes::things::{Error, thumbnail::ThumbnailSize}, blobs, Relations, items::events::update_search_index};
use events::DelayedAction;
pub mod events;
@ -107,8 +107,11 @@ impl Items {
let derived = if let EventContent::File(file) = &event.content {
debug!("begin derive");
let derived = events::derive(self, &event, file).await?;
let (derived, thumb) = events::derive(self, &event, file).await?;
self.db.derived_set(&event.id, &derived).await?;
if let Some(thumb) = thumb {
self.db.thumbnail_create(&event.id, &ThumbnailSize::Raw, &thumb).await?;
}
debug!("derived file info");
derived
} else {

View file

@ -11,7 +11,7 @@ pub mod fetch;
mod query;
mod blob;
mod enumerate;
mod thumbnail;
pub mod thumbnail;
type Response<T> = Result<T, Error>;

View file

@ -2,15 +2,15 @@ use std::fmt::Display;
use axum::extract::{State, Path, Query};
use axum::headers::{ContentType, CacheControl};
use axum::TypedHeader;
use bytes::Bytes;
use crate::ServerState;
use crate::state::db::Database;
use crate::state::db::{Database, Thumbnail};
use crate::routes::util::get_blob;
use serde::{Serialize, Deserialize};
use ufh::item::ItemRef;
use std::io::Cursor;
use std::sync::Arc;
use std::time::Duration;
use sqlx::query as sql;
use super::{Response, Error};
@ -18,44 +18,53 @@ pub async fn route(
State(state): State<Arc<ServerState>>,
Path(item_ref): Path<ItemRef>,
Query(params): Query<ThumbnailParams>,
) -> Response<(TypedHeader<ContentType>, TypedHeader<CacheControl>, Vec<u8>)> {
let item_ref_str = item_ref.to_string();
let thumb_size = ThumbnailSize::closest_to(params.width, params.height);
let thumb_size_str = thumb_size.to_string();
let thumb = sql!("SELECT * FROM thumbnails WHERE ref = ? AND size = ?", item_ref_str, thumb_size_str)
.fetch_optional(state.db.pool())
.await?;
) -> Response<(TypedHeader<ContentType>, TypedHeader<CacheControl>, Bytes)> {
let size = ThumbnailSize::closest_to(params.width, params.height);
let thumb = state.db.thumbnail_get(&item_ref, &size).await?;
let cache_control = TypedHeader(CacheControl::new()
.with_public()
.with_max_age(Duration::from_secs(60 * 60 * 24)));
let content_type = TypedHeader(ContentType::png());
if let Some(thumb) = thumb {
Ok((content_type, cache_control, thumb.blob))
} else {
let event = state.db.get_event(&item_ref).await?.ok_or(Error::NotFound)?;
let blob = get_blob(&state.items, &event, params.via.as_deref()).await?;
let image = image::load_from_memory(&blob)?;
let (width, height) = thumb_size.get_dimensions();
let thumb = image.thumbnail(width, height);
let mut buffer = Cursor::new(Vec::new());
thumb.write_to(&mut buffer, image::ImageFormat::Png)?;
let data = buffer.into_inner();
sql!("INSERT INTO thumbnails (ref, size, blob) VALUES (?, ?, ?)", item_ref_str, thumb_size_str, data)
.execute(state.db.pool())
.await?;
Ok((content_type, cache_control, data))
match thumb {
Thumbnail::Raw(bytes) => {
let thumb = generate_thumb(&bytes, &size)?;
state.db.thumbnail_create(&item_ref, &size, &thumb).await?;
Ok((content_type, cache_control, thumb))
},
Thumbnail::Some(thumb) => {
Ok((content_type, cache_control, thumb))
}
Thumbnail::None => {
let event = state.db.get_event(&item_ref).await?.ok_or(Error::NotFound)?;
let blob = get_blob(&state.items, &event, params.via.as_deref()).await?;
let thumb = generate_thumb(&blob, &size)?;
state.db.thumbnail_create(&item_ref, &size, &thumb).await?;
Ok((content_type, cache_control, thumb))
}
}
}
fn generate_thumb(bytes: &Bytes, size: &ThumbnailSize) -> Result<Bytes, Error> {
let image = image::load_from_memory(bytes)?;
let (width, height) = size.get_dimensions();
let thumb = image.thumbnail(width, height);
let mut buffer = Cursor::new(Vec::new());
thumb.write_to(&mut buffer, image::ImageFormat::Png)?;
Ok(Bytes::from(buffer.into_inner()))
}
// TODO (maybe): no height/width -> return blob
// eg. for getting the full thumbnail for a video without resizing
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ThumbnailParams {
pub height: u32,
pub width: u32,
height: u32,
width: u32,
via: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub enum ThumbnailSize {
Raw,
Small,
Medium,
Large,
@ -67,6 +76,7 @@ impl ThumbnailSize {
ThumbnailSize::Small => (64, 64),
ThumbnailSize::Medium => (256, 256),
ThumbnailSize::Large => (512, 512),
ThumbnailSize::Raw => panic!("raw thumbnails shouldn't be generated"),
}
}
@ -87,6 +97,7 @@ impl Display for ThumbnailSize {
ThumbnailSize::Small => "small",
ThumbnailSize::Medium => "medium",
ThumbnailSize::Large => "large",
ThumbnailSize::Raw => "raw",
})
}
}

View file

@ -1,10 +1,13 @@
use std::collections::HashMap;
use bytes::Bytes;
use ufh::derived::Derived;
use ufh::item::ItemRef;
use ufh::query::Query;
use ufh::event::Event;
use crate::routes::things::thumbnail::ThumbnailSize;
// TODO: make a proper type for shares
// type Share = String;
@ -32,6 +35,13 @@ pub enum DbItem {
Blob,
}
#[derive(Debug)]
pub enum Thumbnail {
Raw(Bytes),
Some(Bytes),
None
}
pub trait Database {
type Error;
@ -50,9 +60,9 @@ pub trait Database {
async fn derived_set(&self, item_ref: &ItemRef, derived: &Derived) -> Result<(), Self::Error>;
// thumbnails
// async fn thumbnail_create(&self, item_ref: &ItemRef, size: &ThumbnailSize, bytes: &[u8]) -> Result<(), Self::Error>;
// async fn thumbnail_get(&self, item_ref: &ItemRef, size: &ThumbnailSize) -> Result<Option<()>, Self::Error>;
// async fn thumbnail_delete(&self, item_ref: &ItemRef, size: &ThumbnailSize) -> Result<(), Self::Error>;
async fn thumbnail_create(&self, item_ref: &ItemRef, size: &ThumbnailSize, bytes: &[u8]) -> Result<(), Self::Error>;
async fn thumbnail_get(&self, item_ref: &ItemRef, size: &ThumbnailSize) -> Result<Thumbnail, Self::Error>;
async fn thumbnail_delete(&self, item_ref: &ItemRef, size: &ThumbnailSize) -> Result<(), Self::Error>;
// shares
// async fn share_create(&self, item_ref: &ItemRef, share_id: Option<&str>, expires_at: Option<u64>) -> Result<(), Self::Error>;

View file

@ -4,6 +4,7 @@
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use bytes::Bytes;
use sqlx::{SqlitePool, sqlite::SqliteConnectOptions, QueryBuilder, Row};
use sqlx::query as sql;
use futures_util::TryStreamExt;
@ -12,7 +13,8 @@ use ufh::event::EventContent;
use ufh::{query, item::ItemRef};
use tracing::debug;
use super::{Database, DbItem};
use super::{Database, DbItem, Thumbnail};
use crate::routes::things::thumbnail::ThumbnailSize;
use crate::{Event, Error};
#[derive(Debug, Clone)]
@ -388,4 +390,36 @@ impl Database for Sqlite {
Ok(())
}
async fn thumbnail_create(&self, item_ref: &ItemRef, size: &ThumbnailSize, bytes: &[u8]) -> Result<(), Self::Error> {
let item_ref_str = item_ref.to_string();
let size_str = size.to_string();
sql!("INSERT INTO thumbnails (ref, size, blob) VALUES (?, ?, ?)", item_ref_str, size_str, bytes)
.execute(&self.pool)
.await?;
Ok(())
}
async fn thumbnail_get(&self, item_ref: &ItemRef, size: &ThumbnailSize) -> Result<Thumbnail, Self::Error> {
let item_ref_str = item_ref.to_string();
let size_str = size.to_string();
let result = sql!("SELECT * FROM thumbnails WHERE ref = ? AND size IN (?, 'raw')", item_ref_str, size_str)
.fetch_one(&self.pool)
.await;
match result {
Ok(row) if row.size == "raw" => Ok(Thumbnail::Raw(Bytes::from(row.blob))),
Ok(row) => Ok(Thumbnail::Some(Bytes::from(row.blob))),
Err(sqlx::error::Error::RowNotFound) => return Ok(Thumbnail::None),
Err(err) => return Err(err.into()),
}
}
async fn thumbnail_delete(&self, item_ref: &ItemRef, size: &ThumbnailSize) -> Result<(), Self::Error> {
let item_ref_str = item_ref.to_string();
let size_str = size.to_string();
sql!("DELETE FROM thumbnails WHERE ref = ? AND size = ?", item_ref_str, size_str)
.execute(&self.pool)
.await?;
Ok(())
}
}