Compare commits

...
Author SHA1 Message Date
FredApps 126d730d6d desktop: use tokio::fs for Sora and Midjourney credential and image files
Resolves SonarCloud rust:S7493 in the Sora credential state and Midjourney download/upload helpers.
2026-11-10 12:05:53 +01:00
AxylumandCédric Legrand 1510abf563 url_utils: satisfy Clippy pedantic lints (#2681)
## Update — 2026-11-01

The latest revision removes the systematic `#[must_use]` additions and
any product-name backticks introduced solely for optional
`clippy::pedantic` compliance. The useful behavior-preserving cleanups
remain. Earlier summary and verification text below describes the
original revision; the latest PR comment records the current checks.

---

## Summary
- resolve all 54 distinct diagnostics in `url_utils`: 17 pedantic, 36
deprecation warnings, and one Eq/Hash contract issue
- preserve the public compatibility module and nested module paths with
narrow documented expectations
- implement `Hash` against the same bare value as `PartialEq`, with a
regression test
- apply the repository two-space rustfmt rules within `url_utils` only

No public API or dependency changes.

## Verification
- `SQLX_OFFLINE=true cargo clippy -p url_utils --all-targets -- -W
clippy::pedantic -D warnings`
- `SQLX_OFFLINE=true cargo test -p url_utils` — 32 unit tests and 2
doctests passed
- `cargo fmt -p url_utils -- --check`
- Independent read-only review: no Rust findings; the untracked planning
file is excluded from the PR

---------

Co-authored-by: Cédric Legrand <cedric@legrand-tech.fr>
2026-11-10 11:32:48 +01:00
Dylan M. Taylor 1218293fe2 docs: add CodeRabbit badge to README (#2790)
Add CodeRabbit review badge to README alongside SonarCloud.
2026-11-09 10:09:12 -05:00
13 changed files with 89 additions and 112 deletions
+1
View File
@@ -2,6 +2,7 @@
<p align="center">
<a href="https://sonarcloud.io/project/overview?id=storytold_artcraft"><img alt="Scanned by SonarCloud" src="https://img.shields.io/badge/Scanned_by-SonarCloud-4b9fd5?style=flat-square&logo=sonarqubecloud&logoColor=white"></a>
<a href="https://coderabbit.ai"><img alt="Reviewed by CodeRabbit" src="https://img.shields.io/coderabbit/prs/github/storytold/artcraft?style=flat-square&label=Reviewed%20by%20CodeRabbit&logo=coderabbit"></a>
</p>
<p align="center"><strong>The IDE for artists.</strong></p>
@@ -119,7 +119,7 @@ pub(super) async fn upload_midjourney_batch(maybe_app_handle: Option<&AppHandle>
}
}
if let Err(err) = std::fs::remove_file(MidjourneyUploadCheckpoint::path(app_data_root, local_task)) {
if let Err(err) = tokio::fs::remove_file(MidjourneyUploadCheckpoint::path(app_data_root, local_task)).await {
log::warn!("Could not remove completed Midjourney upload checkpoint: {}", err);
}
Ok(())
@@ -2,8 +2,6 @@ use crate::core::state::data_dir::app_data_root::AppDataRoot;
use crate::core::state::data_dir::trait_data_subdir::DataSubdir;
use errors::AnyhowResult;
use midjourney_client::utils::get_image_url::get_image_url;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use url::Url;
use midjourney_client::utils::image_downloader_client::ImageDownloaderClient;
@@ -28,8 +26,7 @@ pub async fn download_midjourney_image(
let download_filename = format!("{}_{}.{}", job_id, index, ext);
let download_path = tempdir.join(download_filename);
let mut file = File::create(&download_path)?;
file.write_all(&image_bytes)?;
tokio::fs::write(&download_path, &image_bytes).await?;
Ok(download_path)
}
@@ -14,7 +14,7 @@ pub async fn initialize_sora_jwt_bearer_token(app_data_root: &AppDataRoot) -> An
return Ok(()); // Bearer token already exists.
}
let cookie = std::fs::read_to_string(app_data_root.get_sora_cookie_file_path())?
let cookie = tokio::fs::read_to_string(app_data_root.get_sora_cookie_file_path()).await?
.trim()
.to_string();
@@ -22,7 +22,7 @@ pub async fn initialize_sora_jwt_bearer_token(app_data_root: &AppDataRoot) -> An
let bearer = generate_bearer_jwt_with_cookie(&cookie).await?;
std::fs::write(app_data_root.get_sora_bearer_token_file_path(), bearer)?;
tokio::fs::write(app_data_root.get_sora_bearer_token_file_path(), bearer).await?;
Ok(())
}
@@ -8,8 +8,7 @@ use openai_sora_client::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
use openai_sora_client::creds::sora_sentinel::SoraSentinel;
use openai_sora_client::creds::sora_sentinel_token::SoraSentinelToken;
use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session;
use std::fs::{read_to_string, OpenOptions};
use std::io::Write;
use tokio::fs::{read_to_string, write};
pub async fn read_sora_credentials_from_disk_with_upgrade(app_data_root: &AppDataRoot) -> AnyhowResult<SoraCredentialSet> {
let cookie_file = app_data_root.get_sora_cookie_file_path();
@@ -21,7 +20,7 @@ pub async fn read_sora_credentials_from_disk_with_upgrade(app_data_root: &AppDat
return Err(anyhow!("Cookie file does not exist: {:?}", cookie_file));
}
let value = read_to_string(cookie_file)?
let value = read_to_string(cookie_file).await?
.trim()
.to_string();
@@ -32,14 +31,14 @@ pub async fn read_sora_credentials_from_disk_with_upgrade(app_data_root: &AppDat
let mut sentinel_token = None;
if bearer_file.exists() {
let value = read_to_string(&bearer_file)?
let value = read_to_string(&bearer_file).await?
.trim()
.to_string();
bearer = Some(SoraJwtBearerToken::new(value)?);
}
if legacy_sentinel_file.exists() {
let value = read_to_string(&legacy_sentinel_file)?
let value = read_to_string(&legacy_sentinel_file).await?
.trim()
.to_string();
@@ -47,7 +46,7 @@ pub async fn read_sora_credentials_from_disk_with_upgrade(app_data_root: &AppDat
}
if sentinel_token_file.exists() {
let value = read_to_string(&sentinel_token_file)?
let value = read_to_string(&sentinel_token_file).await?
.trim()
.to_string();
@@ -73,14 +72,7 @@ pub async fn read_sora_credentials_from_disk_with_upgrade(app_data_root: &AppDat
info!("Persisting bearer token value...");
let value = bearer.token_str();
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&bearer_file)?;
file.write_all(value.as_bytes())?;
file.flush()?;
write(&bearer_file, value.as_bytes()).await?;
}
}
@@ -89,14 +81,7 @@ pub async fn read_sora_credentials_from_disk_with_upgrade(app_data_root: &AppDat
info!("Persisting sentinel value...");
let value = sentinel.get_sentinel();
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&legacy_sentinel_file)?;
file.write_all(value.as_bytes())?;
file.flush()?;
write(&legacy_sentinel_file, value.as_bytes()).await?;
}
}
@@ -105,14 +90,7 @@ pub async fn read_sora_credentials_from_disk_with_upgrade(app_data_root: &AppDat
info!("Persisting sentinel token...");
let value = sentinel_token.to_persistent_storage_json()?;
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&sentinel_token_file)?;
file.write_all(value.as_bytes())?;
file.flush()?;
write(&sentinel_token_file, value.as_bytes()).await?;
}
}
@@ -162,14 +162,7 @@ impl SoraCredentialManager {
self.holder.set_credentials(&creds)?;
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(self.app_data_root.get_sora_legacy_sentinel_file_path())?;
file.write_all(sentinel.as_bytes())?;
file.flush()?;
tokio::fs::write(self.app_data_root.get_sora_legacy_sentinel_file_path(), sentinel.as_bytes()).await?;
Ok(creds)
}
@@ -1,4 +1,3 @@
/// 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,9 +12,7 @@ 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)
}
@@ -26,14 +24,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,3 +1,4 @@
#[expect(clippy::module_inception, reason = "Preserve the existing public module path")]
mod download_extension;
pub mod extract_download_extension_from_url;
+29 -12
View File
@@ -1,5 +1,6 @@
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.
///
@@ -16,7 +17,7 @@ use std::fmt;
/// assert_eq!(ext.without_period(), "mp4");
/// assert_eq!(ext.with_period(), ".mp4");
/// ```
#[derive(Clone, Eq, Hash)]
#[derive(Clone, Eq)]
pub struct Extension {
/// The extension without the leading period, e.g. "png", "mp4".
value: Cow<'static, str>,
@@ -34,20 +35,14 @@ 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"`.
@@ -67,6 +62,12 @@ 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)
@@ -135,12 +136,28 @@ 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\")");
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()
}
}
@@ -6,24 +6,13 @@ 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 {
@@ -62,9 +51,7 @@ 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);
@@ -72,27 +59,40 @@ 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 }
}
ExtractExtensions::KnownImage => {
if slice_contains(KNOWN_IMAGE_EXTENSIONS, &candidate) { Some(candidate) } else { None }
}
ExtractExtensions::KnownAudio => {
if slice_contains(KNOWN_AUDIO_EXTENSIONS, &candidate) { Some(candidate) } else { None }
}
ExtractExtensions::KnownVideo => {
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 set.contains(&candidate) {
Some(candidate)
} else {
None
}
}
},
ExtractExtensions::KnownImage => {
if slice_contains(KNOWN_IMAGE_EXTENSIONS, &candidate) {
Some(candidate)
} else {
None
}
},
ExtractExtensions::KnownAudio => {
if slice_contains(KNOWN_AUDIO_EXTENSIONS, &candidate) {
Some(candidate)
} else {
None
}
},
ExtractExtensions::KnownVideo => {
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) {
Some(candidate)
} else {
None
}
},
}
}
@@ -190,19 +190,14 @@ 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());
}
@@ -215,10 +210,7 @@ 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");
}
@@ -230,8 +222,7 @@ 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]
+2 -1
View File
@@ -1,2 +1,3 @@
#[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;
+2 -1
View File
@@ -1,2 +1,3 @@
pub mod extension;
pub mod download_extension;
#[expect(deprecated, reason = "Keep the public compatibility module while downstream users migrate to Extension")]
pub mod download_extension;