mirror of
https://git.asonix.dog/asonix/pict-rs
synced 2024-12-23 03:41:23 +00:00
Keep better track of gif/mp4 thumbnail
This commit is contained in:
parent
1767942e31
commit
f67aeb92fa
4 changed files with 132 additions and 118 deletions
|
@ -6,12 +6,6 @@ pub(crate) struct Error {
|
||||||
kind: UploadError,
|
kind: UploadError,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
|
||||||
pub(crate) fn kind(&self) -> &UploadError {
|
|
||||||
&self.kind
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for Error {
|
impl std::fmt::Debug for Error {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{}\n", self.kind)
|
write!(f, "{}\n", self.kind)
|
||||||
|
@ -75,6 +69,9 @@ pub(crate) enum UploadError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
StripPrefix(#[from] std::path::StripPrefixError),
|
StripPrefix(#[from] std::path::StripPrefixError),
|
||||||
|
|
||||||
|
#[error("Provided process path is invalid")]
|
||||||
|
ParsePath,
|
||||||
|
|
||||||
#[error("Failed to acquire the semaphore")]
|
#[error("Failed to acquire the semaphore")]
|
||||||
Semaphore,
|
Semaphore,
|
||||||
|
|
||||||
|
|
33
src/main.rs
33
src/main.rs
|
@ -388,7 +388,7 @@ async fn prepare_process(
|
||||||
let processed_name = format!("{}.{}", name, ext);
|
let processed_name = format!("{}.{}", name, ext);
|
||||||
|
|
||||||
let (thumbnail_path, thumbnail_args) =
|
let (thumbnail_path, thumbnail_args) =
|
||||||
self::processor::build_chain(&operations, processed_name);
|
self::processor::build_chain(&operations, processed_name)?;
|
||||||
|
|
||||||
Ok((format, name, thumbnail_path, thumbnail_args))
|
Ok((format, name, thumbnail_path, thumbnail_args))
|
||||||
}
|
}
|
||||||
|
@ -465,40 +465,11 @@ async fn process(
|
||||||
return ranged_file_resp(real_path, range, details).await;
|
return ranged_file_resp(real_path, range, details).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut original_path = manager.path_from_filename(name.clone()).await?;
|
let original_path = manager.still_path_from_filename(name.clone()).await?;
|
||||||
|
|
||||||
let thumbnail_path2 = thumbnail_path.clone();
|
let thumbnail_path2 = thumbnail_path.clone();
|
||||||
let process_fut = async {
|
let process_fut = async {
|
||||||
let thumbnail_path = thumbnail_path2;
|
let thumbnail_path = thumbnail_path2;
|
||||||
// Create and save a JPG for motion images (gif, mp4)
|
|
||||||
if let Some((updated_path, exists)) =
|
|
||||||
self::processor::prepare_image(original_path.clone()).await?
|
|
||||||
{
|
|
||||||
original_path = updated_path.clone();
|
|
||||||
|
|
||||||
if exists.is_new() {
|
|
||||||
// Save the transcoded file in another task
|
|
||||||
debug!("Spawning storage task");
|
|
||||||
let manager2 = manager.clone();
|
|
||||||
let name = name.clone();
|
|
||||||
let span = tracing::info_span!(
|
|
||||||
parent: None,
|
|
||||||
"Storing variant info",
|
|
||||||
path = &tracing::field::debug(&updated_path),
|
|
||||||
name = &tracing::field::display(&name),
|
|
||||||
);
|
|
||||||
span.follows_from(Span::current());
|
|
||||||
actix_rt::spawn(
|
|
||||||
async move {
|
|
||||||
if let Err(e) = manager2.store_variant(None, &updated_path, &name).await {
|
|
||||||
error!("Error storing variant, {}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.instrument(span),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let permit = PROCESS_SEMAPHORE.acquire().await?;
|
let permit = PROCESS_SEMAPHORE.acquire().await?;
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
use crate::{
|
use crate::error::{Error, UploadError};
|
||||||
error::{Error, UploadError},
|
use std::path::PathBuf;
|
||||||
ffmpeg::ThumbnailFormat,
|
use tracing::instrument;
|
||||||
};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use tracing::{debug, error, instrument};
|
|
||||||
|
|
||||||
fn ptos(path: &Path) -> Result<String, Error> {
|
|
||||||
Ok(path.to_str().ok_or(UploadError::Path)?.to_owned())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) trait Processor {
|
pub(crate) trait Processor {
|
||||||
const NAME: &'static str;
|
const NAME: &'static str;
|
||||||
|
@ -164,88 +157,41 @@ impl Processor for Blur {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
pub(crate) fn build_chain(args: &[(String, String)], filename: String) -> (PathBuf, Vec<String>) {
|
pub(crate) fn build_chain(
|
||||||
fn parse<P: Processor>(key: &str, value: &str) -> Option<P> {
|
args: &[(String, String)],
|
||||||
|
filename: String,
|
||||||
|
) -> Result<(PathBuf, Vec<String>), Error> {
|
||||||
|
fn parse<P: Processor>(key: &str, value: &str) -> Result<Option<P>, UploadError> {
|
||||||
if key == P::NAME {
|
if key == P::NAME {
|
||||||
return P::parse(key, value);
|
return Ok(Some(P::parse(key, value).ok_or(UploadError::ParsePath)?));
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! parse {
|
macro_rules! parse {
|
||||||
($inner:expr, $x:ident, $k:expr, $v:expr) => {{
|
($inner:expr, $x:ident, $k:expr, $v:expr) => {{
|
||||||
if let Some(processor) = parse::<$x>($k, $v) {
|
if let Some(processor) = parse::<$x>($k, $v)? {
|
||||||
return (processor.path($inner.0), processor.command($inner.1));
|
return Ok((processor.path($inner.0), processor.command($inner.1)));
|
||||||
};
|
};
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
let (path, args) =
|
let (path, args) =
|
||||||
args.into_iter()
|
args.into_iter()
|
||||||
.fold((PathBuf::default(), vec![]), |inner, (name, value)| {
|
.fold(Ok((PathBuf::default(), vec![])), |inner, (name, value)| {
|
||||||
|
if let Ok(inner) = inner {
|
||||||
parse!(inner, Identity, name, value);
|
parse!(inner, Identity, name, value);
|
||||||
parse!(inner, Thumbnail, name, value);
|
parse!(inner, Thumbnail, name, value);
|
||||||
parse!(inner, Resize, name, value);
|
parse!(inner, Resize, name, value);
|
||||||
parse!(inner, Crop, name, value);
|
parse!(inner, Crop, name, value);
|
||||||
parse!(inner, Blur, name, value);
|
parse!(inner, Blur, name, value);
|
||||||
|
|
||||||
debug!("Skipping {}: {}, invalid", name, value);
|
Err(Error::from(UploadError::ParsePath))
|
||||||
|
} else {
|
||||||
inner
|
inner
|
||||||
});
|
|
||||||
|
|
||||||
(path.join(filename), args)
|
|
||||||
}
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
fn is_motion(s: &str) -> bool {
|
Ok((path.join(filename), args))
|
||||||
s.ends_with(".gif") || s.ends_with(".mp4")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) enum Exists {
|
|
||||||
Exists,
|
|
||||||
New,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Exists {
|
|
||||||
pub(crate) fn is_new(&self) -> bool {
|
|
||||||
matches!(self, Exists::New)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn prepare_image(
|
|
||||||
original_file: PathBuf,
|
|
||||||
) -> Result<Option<(PathBuf, Exists)>, Error> {
|
|
||||||
let original_path_str = ptos(&original_file)?;
|
|
||||||
let jpg_path = format!("{}.jpg", original_path_str);
|
|
||||||
let jpg_path = PathBuf::from(jpg_path);
|
|
||||||
|
|
||||||
if tokio::fs::metadata(&jpg_path).await.is_ok() {
|
|
||||||
return Ok(Some((jpg_path, Exists::Exists)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_motion(&original_path_str) {
|
|
||||||
let orig_path = original_path_str.clone();
|
|
||||||
|
|
||||||
let tmpfile = crate::tmp_file();
|
|
||||||
crate::safe_create_parent(&tmpfile).await?;
|
|
||||||
|
|
||||||
let res = crate::ffmpeg::thumbnail(orig_path, &tmpfile, ThumbnailFormat::Jpeg).await;
|
|
||||||
|
|
||||||
if let Err(e) = res {
|
|
||||||
error!("transcode error: {:?}", e);
|
|
||||||
tokio::fs::remove_file(&tmpfile).await?;
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return match crate::safe_move_file(tmpfile, jpg_path.clone()).await {
|
|
||||||
Err(e) if matches!(e.kind(), UploadError::FileExists) => {
|
|
||||||
Ok(Some((jpg_path, Exists::Exists)))
|
|
||||||
}
|
|
||||||
Err(e) => Err(e),
|
|
||||||
_ => Ok(Some((jpg_path, Exists::New))),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Format,
|
config::Format,
|
||||||
error::{Error, UploadError},
|
error::{Error, UploadError},
|
||||||
|
ffmpeg::ThumbnailFormat,
|
||||||
migrate::{alias_id_key, alias_key, alias_key_bounds, LatestDb},
|
migrate::{alias_id_key, alias_key, alias_key_bounds, LatestDb},
|
||||||
};
|
};
|
||||||
use actix_web::web;
|
use actix_web::web;
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
use std::{path::PathBuf, sync::Arc};
|
use std::{
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
path::PathBuf,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
use storage_path_generator::{Generator, Path};
|
use storage_path_generator::{Generator, Path};
|
||||||
use tracing::{debug, error, info, instrument, warn, Span};
|
use tracing::{debug, error, info, instrument, warn, Span};
|
||||||
use tracing_futures::Instrument;
|
use tracing_futures::Instrument;
|
||||||
|
@ -34,6 +39,7 @@ pub(super) use session::UploadManagerSession;
|
||||||
// - Path Tree
|
// - Path Tree
|
||||||
// - filename -> relative path
|
// - filename -> relative path
|
||||||
// - filename / relative variant path -> relative variant path
|
// - filename / relative variant path -> relative variant path
|
||||||
|
// - filename / motion -> relative motion path
|
||||||
// - Settings Tree
|
// - Settings Tree
|
||||||
// - last-path -> last generated path
|
// - last-path -> last generated path
|
||||||
// - fs-restructure-01-complete -> bool
|
// - fs-restructure-01-complete -> bool
|
||||||
|
@ -111,6 +117,76 @@ impl UploadManager {
|
||||||
Ok(manager)
|
Ok(manager)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn still_path_from_filename(
|
||||||
|
&self,
|
||||||
|
filename: String,
|
||||||
|
) -> Result<PathBuf, Error> {
|
||||||
|
let path = self.path_from_filename(filename.clone()).await?;
|
||||||
|
let details =
|
||||||
|
if let Some(details) = self.variant_details(path.clone(), filename.clone()).await? {
|
||||||
|
details
|
||||||
|
} else {
|
||||||
|
Details::from_path(&path).await?
|
||||||
|
};
|
||||||
|
|
||||||
|
if !details.is_motion() {
|
||||||
|
return Ok(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(motion_path) = self.motion_path(&filename).await? {
|
||||||
|
return Ok(motion_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let jpeg_path = self.next_directory()?.join(&filename);
|
||||||
|
crate::safe_create_parent(&jpeg_path).await?;
|
||||||
|
|
||||||
|
let permit = crate::PROCESS_SEMAPHORE.acquire().await;
|
||||||
|
let res = crate::ffmpeg::thumbnail(&path, &jpeg_path, ThumbnailFormat::Jpeg).await;
|
||||||
|
drop(permit);
|
||||||
|
|
||||||
|
if let Err(e) = res {
|
||||||
|
error!("transcode error: {:?}", e);
|
||||||
|
self.remove_path(&jpeg_path).await?;
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.store_motion_path(&filename, &jpeg_path).await?;
|
||||||
|
Ok(jpeg_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn motion_path(&self, filename: &str) -> Result<Option<PathBuf>, Error> {
|
||||||
|
let path_tree = self.inner.path_tree.clone();
|
||||||
|
let motion_key = format!("{}/motion", filename);
|
||||||
|
|
||||||
|
let opt = web::block(move || path_tree.get(motion_key.as_bytes())).await??;
|
||||||
|
|
||||||
|
if let Some(ivec) = opt {
|
||||||
|
return Ok(Some(
|
||||||
|
self.inner.root_dir.join(String::from_utf8(ivec.to_vec())?),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn store_motion_path(
|
||||||
|
&self,
|
||||||
|
filename: &str,
|
||||||
|
path: impl AsRef<std::path::Path>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let path_bytes = self
|
||||||
|
.generalize_path(path.as_ref())?
|
||||||
|
.to_str()
|
||||||
|
.ok_or(UploadError::Path)?
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec();
|
||||||
|
let motion_key = format!("{}/motion", filename);
|
||||||
|
let path_tree = self.inner.path_tree.clone();
|
||||||
|
|
||||||
|
web::block(move || path_tree.insert(motion_key.as_bytes(), path_bytes)).await??;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
pub(crate) async fn path_from_filename(&self, filename: String) -> Result<PathBuf, Error> {
|
pub(crate) async fn path_from_filename(&self, filename: String) -> Result<PathBuf, Error> {
|
||||||
let path_tree = self.inner.path_tree.clone();
|
let path_tree = self.inner.path_tree.clone();
|
||||||
|
@ -125,7 +201,12 @@ impl UploadManager {
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn store_path(&self, filename: String, path: &std::path::Path) -> Result<(), Error> {
|
async fn store_path(&self, filename: String, path: &std::path::Path) -> Result<(), Error> {
|
||||||
let path_bytes = path.to_str().ok_or(UploadError::Path)?.as_bytes().to_vec();
|
let path_bytes = self
|
||||||
|
.generalize_path(path)?
|
||||||
|
.to_str()
|
||||||
|
.ok_or(UploadError::Path)?
|
||||||
|
.as_bytes()
|
||||||
|
.to_vec();
|
||||||
let path_tree = self.inner.path_tree.clone();
|
let path_tree = self.inner.path_tree.clone();
|
||||||
web::block(move || path_tree.insert(filename.as_bytes(), path_bytes)).await??;
|
web::block(move || path_tree.insert(filename.as_bytes(), path_bytes)).await??;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -536,6 +617,11 @@ impl<T> Serde<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Details {
|
impl Details {
|
||||||
|
fn is_motion(&self) -> bool {
|
||||||
|
self.content_type.type_() == "video"
|
||||||
|
|| self.content_type.type_() == "image" && self.content_type.subtype() == "gif"
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument("Details from bytes", skip(input))]
|
#[tracing::instrument("Details from bytes", skip(input))]
|
||||||
pub(crate) async fn from_bytes(input: web::Bytes) -> Result<Self, Error> {
|
pub(crate) async fn from_bytes(input: web::Bytes) -> Result<Self, Error> {
|
||||||
let details = crate::magick::details_bytes(input).await?;
|
let details = crate::magick::details_bytes(input).await?;
|
||||||
|
@ -606,6 +692,20 @@ fn delete_key(alias: &str) -> String {
|
||||||
format!("{}/delete", alias)
|
format!("{}/delete", alias)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> Deref for Serde<T> {
|
||||||
|
type Target = T;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> DerefMut for Serde<T> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for UploadManager {
|
impl std::fmt::Debug for UploadManager {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
f.debug_struct("UploadManager").finish()
|
f.debug_struct("UploadManager").finish()
|
||||||
|
|
Loading…
Reference in a new issue