mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-10 07:56:46 -05:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
218fd97a2e |
@@ -33,7 +33,6 @@ chrono.workspace = true
|
||||
cookie.workspace = true
|
||||
log.workspace = true
|
||||
reqwest = { workspace = true, features = ["multipart", "gzip"] }
|
||||
tokio = { workspace = true, features = ["fs"] }
|
||||
url.workspace = true
|
||||
|
||||
# Serialization
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ pub async fn legacy_upload_media_file_from_file<P: AsRef<Path>>(
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let file_bytes = tokio::fs::read(args.path.as_ref()).await
|
||||
let file_bytes = std::fs::read(args.path.as_ref())
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
let file_name = args.path.as_ref().file_name()
|
||||
.and_then(|n| n.to_str()).unwrap_or("file").to_string();
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ pub async fn upload_image_media_file_from_file_with_idempotency_token<P: AsRef<P
|
||||
.build()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
|
||||
let file_bytes = tokio::fs::read(args.path.as_ref()).await
|
||||
let file_bytes = std::fs::read(args.path.as_ref())
|
||||
.map_err(|err| StorytellerError::Client(ClientError::from(err)))?;
|
||||
let file_name = args.path.as_ref().file_name()
|
||||
.and_then(|n| n.to_str()).unwrap_or("file").to_string();
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ pub async fn upload_new_engine_asset_from_file<P: AsRef<Path>>(
|
||||
let client = storyteller_client_builder(api_host)
|
||||
.build()?;
|
||||
|
||||
let file_bytes = tokio::fs::read(path.as_ref()).await?;
|
||||
let file_bytes = std::fs::read(path.as_ref())?;
|
||||
let file_name = path.as_ref().file_name()
|
||||
.and_then(|n| n.to_str()).unwrap_or("file").to_string();
|
||||
let mut form = Form::new()
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ pub async fn upload_video_media_file_from_file<P: AsRef<Path>>(
|
||||
let client = storyteller_client_builder(args.api_host)
|
||||
.build()?;
|
||||
|
||||
let file_bytes = tokio::fs::read(args.path.as_ref()).await?;
|
||||
let file_bytes = std::fs::read(args.path.as_ref())?;
|
||||
let file_name = args.path.as_ref().file_name()
|
||||
.and_then(|n| n.to_str()).unwrap_or("file").to_string();
|
||||
let mut form = Form::new()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use log::{info, warn};
|
||||
@@ -66,7 +68,13 @@ pub async fn download_media_file<P: AsRef<Path>>(
|
||||
let bytes = download_bytes(cdn_url).await?;
|
||||
|
||||
// 4. Write to disk.
|
||||
tokio::fs::write(&output_path, &bytes).await
|
||||
let mut file = fs::File::create(&output_path)
|
||||
.map_err(|err| StorytellerError::Client(ClientError::IoError(err)))?;
|
||||
|
||||
file.write_all(&bytes)
|
||||
.map_err(|err| StorytellerError::Client(ClientError::IoError(err)))?;
|
||||
|
||||
file.flush()
|
||||
.map_err(|err| StorytellerError::Client(ClientError::IoError(err)))?;
|
||||
|
||||
info!("Downloaded {} bytes to {:?}", bytes.len(), output_path);
|
||||
|
||||
@@ -31,7 +31,7 @@ reqwest.workspace = true
|
||||
serde.workspace = true
|
||||
wreq.workspace = true
|
||||
serde_derive.workspace = true
|
||||
tokio = { workspace = true, features = ["fs"] }
|
||||
tokio.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ pub(crate) async fn upload_to_kinovi_web(
|
||||
.unwrap_or_else(|| "jpg".to_string());
|
||||
|
||||
let file_bytes = match maybe_local_path {
|
||||
Some(local_path) => match tokio::fs::read(local_path).await {
|
||||
Some(local_path) => match std::fs::read(local_path) {
|
||||
Ok(bytes) => {
|
||||
info!("Reusing predownloaded file for kinovi upload: {}", source_url);
|
||||
bytes
|
||||
|
||||
@@ -93,7 +93,7 @@ impl MidjourneyCredentialManager {
|
||||
let _guard = self.mutation.lock().await;
|
||||
// Remove every format, so a later startup cannot migrate a logged-out account.
|
||||
for path in self.legacy_paths.iter().chain([&self.credential_path]) {
|
||||
match tokio::fs::remove_file(path).await {
|
||||
match std::fs::remove_file(path) {
|
||||
Ok(()) => {},
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {},
|
||||
Err(err) => return Err(err.into()),
|
||||
|
||||
+8
-4
@@ -11,7 +11,7 @@ use errors::AnyhowResult;
|
||||
use log::{debug, error, warn};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::path::Path;
|
||||
use std::{fs::File, io::Write, path::Path};
|
||||
use url::Url;
|
||||
use wreq::Client;
|
||||
|
||||
@@ -271,7 +271,9 @@ pub async fn get_image_gen_status(status_request: &StatusRequest, credentials: &
|
||||
|
||||
pub async fn save_generations_to_dir(generations: &[Generation], dir: &str) -> AnyhowResult<()> {
|
||||
let dir = Path::new(dir);
|
||||
tokio::fs::create_dir_all(dir).await?;
|
||||
if !dir.exists() {
|
||||
std::fs::create_dir_all(dir)?;
|
||||
}
|
||||
for generation in generations {
|
||||
let response = wreq::get(generation.url.clone()).send().await?;
|
||||
let image_bytes = response.bytes().await?;
|
||||
@@ -280,13 +282,15 @@ pub async fn save_generations_to_dir(generations: &[Generation], dir: &str) -> A
|
||||
let ext = url.path().split(".").last().unwrap_or("png");
|
||||
let path = Path::new(dir).join(format!("{}.{}", generation.id, ext));
|
||||
|
||||
tokio::fs::write(path, &image_bytes).await?;
|
||||
let mut file = File::create(path)?;
|
||||
file.write_all(&image_bytes)?;
|
||||
|
||||
let thumbnail_url = generation.encodings.thumbnail.path.clone();
|
||||
let thumbnail_response = wreq::get(thumbnail_url).send().await?;
|
||||
let thumbnail_bytes = thumbnail_response.bytes().await?;
|
||||
let thumbnail_path = Path::new(dir).join(format!("{}_thumbnail.{}", generation.id, ext));
|
||||
tokio::fs::write(thumbnail_path, &thumbnail_bytes).await?;
|
||||
let mut thumbnail_file = File::create(thumbnail_path)?;
|
||||
thumbnail_file.write_all(&thumbnail_bytes)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
/// When downloading a remote file, it makes sense to try to match the same file extension (with reasonable safeguards).
|
||||
#[deprecated(note = "Use `Extension` in `url_utils::extension::extension` instead")]
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
|
||||
@@ -12,7 +12,9 @@ pub fn extract_download_extension_from_url_str(url: &str) -> Option<DownloadExte
|
||||
#[deprecated(note = "Use `extract_extension_from_url` in `url_utils::extension::extract_extension_from_url` instead")]
|
||||
pub fn extract_download_extension_from_url(url: &Url) -> Option<DownloadExtension> {
|
||||
let path = url.path();
|
||||
let extension = std::path::Path::new(path).extension().and_then(|ext| ext.to_str())?;
|
||||
let extension = std::path::Path::new(path)
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())?;
|
||||
DownloadExtension::try_from_str(extension)
|
||||
}
|
||||
|
||||
@@ -24,14 +26,14 @@ mod tests {
|
||||
#[test]
|
||||
fn success_case() {
|
||||
let url = "https://storyteller.ai/example.jpg";
|
||||
let maybe_extension = extract_download_extension_from_url_str(url);
|
||||
let maybe_extension= extract_download_extension_from_url_str(url);
|
||||
assert_eq!(maybe_extension, Some(DownloadExtension::Jpg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn difficult_url() {
|
||||
let url = "https://videos.youtube.com/foo/bar/files/00000000-f19a-1275-a092-caa60b38f8d2%2Fraw?se=2025-11-21T22%3A26%3A53Z&sp=r&sv=2024-08-09&sr=b&skoid=8cfff87a-01fc-27c1-9091-32999dcd1389&sktid=aa8ccaf6-e6da-484e-ab11-9c7496c21c53&skt=2025-11-17T21%3A09%3A08Z&ske=2025-10-23T21%3A14%3A08Z&sks=b&skv=2024-08-04&sig=T2U6%2Brw9WJdJagBkCL4/GlFr72KhZGP3QPlobIO7JlU%3D&ac=oaisdsorprnorthcentralus";
|
||||
let maybe_extension = extract_download_extension_from_url_str(url);
|
||||
let maybe_extension= extract_download_extension_from_url_str(url);
|
||||
assert!(maybe_extension.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#[expect(clippy::module_inception, reason = "Preserve the existing public module path")]
|
||||
mod download_extension;
|
||||
pub mod extract_download_extension_from_url;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
/// A file extension that can be either a compile-time constant or a runtime-parsed value.
|
||||
///
|
||||
@@ -17,7 +16,7 @@ use std::hash::{Hash, Hasher};
|
||||
/// assert_eq!(ext.without_period(), "mp4");
|
||||
/// assert_eq!(ext.with_period(), ".mp4");
|
||||
/// ```
|
||||
#[derive(Clone, Eq)]
|
||||
#[derive(Clone, Eq, Hash)]
|
||||
pub struct Extension {
|
||||
/// The extension without the leading period, e.g. "png", "mp4".
|
||||
value: Cow<'static, str>,
|
||||
@@ -35,14 +34,20 @@ impl Extension {
|
||||
/// const MP4: Extension = Extension::from_static("mp4", ".mp4");
|
||||
/// ```
|
||||
pub const fn from_static(ext: &'static str, dotted: &'static str) -> Self {
|
||||
Self { value: Cow::Borrowed(ext), dotted: Cow::Borrowed(dotted) }
|
||||
Self {
|
||||
value: Cow::Borrowed(ext),
|
||||
dotted: Cow::Borrowed(dotted),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an `Extension` from a `&str` at runtime.
|
||||
/// The input must NOT include a leading period.
|
||||
pub fn new(ext: &str) -> Self {
|
||||
let dotted = format!(".{ext}");
|
||||
Self { value: Cow::Owned(ext.to_string()), dotted: Cow::Owned(dotted) }
|
||||
let dotted = format!(".{}", ext);
|
||||
Self {
|
||||
value: Cow::Owned(ext.to_string()),
|
||||
dotted: Cow::Owned(dotted),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the extension without the leading period, e.g. `"png"`.
|
||||
@@ -62,12 +67,6 @@ impl PartialEq for Extension {
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Extension {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.value.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Extension {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Extension(\"{}\")", self.value)
|
||||
@@ -136,28 +135,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
assert_eq!(format!("{PNG}"), "png");
|
||||
assert_eq!(format!("{}", PNG), "png");
|
||||
assert_eq!(format!("{}", Extension::new("xyz")), "xyz");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug() {
|
||||
assert_eq!(format!("{PNG:?}"), "Extension(\"png\")");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_matches_equality() {
|
||||
let plain = Extension::from_static("png", ".png");
|
||||
let inconsistent = Extension::from_static("png", ".not-a-suffix");
|
||||
assert_eq!(plain, inconsistent);
|
||||
assert_eq!(hash(&plain), hash(&inconsistent));
|
||||
}
|
||||
|
||||
fn hash(extension: &Extension) -> u64 {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
extension.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
assert_eq!(format!("{:?}", PNG), "Extension(\"png\")");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,24 @@ use url::Url;
|
||||
// TODO: Lookups against these as lists will be slow
|
||||
|
||||
/// Known image extensions.
|
||||
const KNOWN_IMAGE_EXTENSIONS: &[Extension] = &[Extension::from_static("png", ".png"), Extension::from_static("jpg", ".jpg"), Extension::from_static("jpeg", ".jpeg"), Extension::from_static("webp", ".webp")];
|
||||
const KNOWN_IMAGE_EXTENSIONS: &[Extension] = &[
|
||||
Extension::from_static("png", ".png"),
|
||||
Extension::from_static("jpg", ".jpg"),
|
||||
Extension::from_static("jpeg", ".jpeg"),
|
||||
Extension::from_static("webp", ".webp"),
|
||||
];
|
||||
|
||||
/// Known audio extensions.
|
||||
const KNOWN_AUDIO_EXTENSIONS: &[Extension] = &[Extension::from_static("wav", ".wav"), Extension::from_static("mp3", ".mp3")];
|
||||
const KNOWN_AUDIO_EXTENSIONS: &[Extension] = &[
|
||||
Extension::from_static("wav", ".wav"),
|
||||
Extension::from_static("mp3", ".mp3"),
|
||||
];
|
||||
|
||||
/// Known video extensions.
|
||||
const KNOWN_VIDEO_EXTENSIONS: &[Extension] = &[Extension::from_static("mp4", ".mp4"), Extension::from_static("webm", ".webm")];
|
||||
const KNOWN_VIDEO_EXTENSIONS: &[Extension] = &[
|
||||
Extension::from_static("mp4", ".mp4"),
|
||||
Extension::from_static("webm", ".webm"),
|
||||
];
|
||||
|
||||
/// Controls which extensions are accepted when extracting from a URL.
|
||||
pub enum ExtractExtensions {
|
||||
@@ -51,7 +62,9 @@ pub fn extract_extension_from_url_str(url: &str, accept: &ExtractExtensions) ->
|
||||
/// Extracts a file extension from a parsed `Url`, filtered by `accept`.
|
||||
pub fn extract_extension_from_url(url: &Url, accept: &ExtractExtensions) -> Option<Extension> {
|
||||
let path = url.path();
|
||||
let raw_ext = std::path::Path::new(path).extension().and_then(|ext| ext.to_str())?;
|
||||
let raw_ext = std::path::Path::new(path)
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())?;
|
||||
|
||||
let lower = raw_ext.to_lowercase();
|
||||
let candidate = Extension::new(&lower);
|
||||
@@ -59,40 +72,27 @@ pub fn extract_extension_from_url(url: &Url, accept: &ExtractExtensions) -> Opti
|
||||
match accept {
|
||||
ExtractExtensions::All => Some(candidate),
|
||||
ExtractExtensions::Set(set) => {
|
||||
if set.contains(&candidate) {
|
||||
Some(candidate)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
if set.contains(&candidate) { Some(candidate) } else { None }
|
||||
}
|
||||
ExtractExtensions::KnownImage => {
|
||||
if slice_contains(KNOWN_IMAGE_EXTENSIONS, &candidate) {
|
||||
Some(candidate)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
if slice_contains(KNOWN_IMAGE_EXTENSIONS, &candidate) { Some(candidate) } else { None }
|
||||
}
|
||||
ExtractExtensions::KnownAudio => {
|
||||
if slice_contains(KNOWN_AUDIO_EXTENSIONS, &candidate) {
|
||||
Some(candidate)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
if slice_contains(KNOWN_AUDIO_EXTENSIONS, &candidate) { Some(candidate) } else { None }
|
||||
}
|
||||
ExtractExtensions::KnownVideo => {
|
||||
if slice_contains(KNOWN_VIDEO_EXTENSIONS, &candidate) {
|
||||
Some(candidate)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
if slice_contains(KNOWN_VIDEO_EXTENSIONS, &candidate) { Some(candidate) } else { None }
|
||||
}
|
||||
ExtractExtensions::KnownMedia => {
|
||||
if slice_contains(KNOWN_IMAGE_EXTENSIONS, &candidate) || slice_contains(KNOWN_AUDIO_EXTENSIONS, &candidate) || slice_contains(KNOWN_VIDEO_EXTENSIONS, &candidate) {
|
||||
if slice_contains(KNOWN_IMAGE_EXTENSIONS, &candidate)
|
||||
|| slice_contains(KNOWN_AUDIO_EXTENSIONS, &candidate)
|
||||
|| slice_contains(KNOWN_VIDEO_EXTENSIONS, &candidate)
|
||||
{
|
||||
Some(candidate)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,14 +190,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn set_matches_custom_list() {
|
||||
let accept = ExtractExtensions::from_vec(vec![Extension::from_static("csv", ".csv"), Extension::from_static("tsv", ".tsv")]);
|
||||
let accept = ExtractExtensions::from_vec(vec![
|
||||
Extension::from_static("csv", ".csv"),
|
||||
Extension::from_static("tsv", ".tsv"),
|
||||
]);
|
||||
let ext = extract_extension_from_url_str("https://example.com/data.csv", &accept);
|
||||
assert_eq!(ext.unwrap().without_period(), "csv");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_rejects_non_member() {
|
||||
let accept = ExtractExtensions::from_vec(vec![Extension::from_static("csv", ".csv")]);
|
||||
let accept = ExtractExtensions::from_vec(vec![
|
||||
Extension::from_static("csv", ".csv"),
|
||||
]);
|
||||
let ext = extract_extension_from_url_str("https://example.com/data.png", &accept);
|
||||
assert!(ext.is_none());
|
||||
}
|
||||
@@ -210,7 +215,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn query_params_ignored() {
|
||||
let ext = extract_extension_from_url_str("https://example.com/file.mp4?token=abc&format=hd", &ExtractExtensions::KnownVideo);
|
||||
let ext = extract_extension_from_url_str(
|
||||
"https://example.com/file.mp4?token=abc&format=hd",
|
||||
&ExtractExtensions::KnownVideo,
|
||||
);
|
||||
assert_eq!(ext.unwrap().without_period(), "mp4");
|
||||
}
|
||||
|
||||
@@ -222,7 +230,8 @@ mod tests {
|
||||
}
|
||||
|
||||
fn extract_extension_all(url: &str) -> Option<String> {
|
||||
extract_extension_from_url_str(url, &ExtractExtensions::All).map(|ext| ext.without_period().to_string())
|
||||
extract_extension_from_url_str(url, &ExtractExtensions::All)
|
||||
.map(|ext| ext.without_period().to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
#[expect(clippy::module_inception, reason = "Preserve the existing public module path")]
|
||||
pub mod extension;
|
||||
pub mod extract_extension_from_url;
|
||||
pub mod extract_extension_from_url;
|
||||
@@ -1,3 +1,2 @@
|
||||
pub mod extension;
|
||||
#[expect(deprecated, reason = "Keep the public compatibility module while downstream users migrate to Extension")]
|
||||
pub mod download_extension;
|
||||
pub mod download_extension;
|
||||
Reference in New Issue
Block a user