Skip to content

Commit 7f49a5e

Browse files
committed
fix: wrong custom section id return type and imgage migration error
1 parent 896979c commit 7f49a5e

2 files changed

Lines changed: 144 additions & 65 deletions

File tree

src/routes/api/v1/search_sections.rs

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ use crate::{
99
api_response::V1ApiResponseTrait,
1010
v1::guards::{AuthenticatedUser, UserRole},
1111
},
12+
utils::id::Id,
1213
};
1314

1415
#[derive(Serialize, sqlx::FromRow)]
1516
pub struct V1SearchSectionResponse {
16-
pub id: i64,
17+
pub id: Id,
1718
pub title: String,
1819
pub section_type: String,
1920
pub smart_filter: Option<String>,
@@ -28,7 +29,7 @@ impl V1ApiResponseTrait for V1SearchSectionResponse {}
2829
pub async fn get_search_sections(
2930
_user: AuthenticatedUser,
3031
) -> V1ApiResponseType<Vec<V1SearchSectionResponse>> {
31-
let sections = sqlx::query!(
32+
let sections = sqlx::query!(
3233
"SELECT id, title, section_type, smart_filter, filter_value, order_index FROM search_sections ORDER BY order_index ASC"
3334
)
3435
.fetch_all(&*SQL)
@@ -38,12 +39,12 @@ pub async fn get_search_sections(
3839
V1ApiError::InternalError
3940
})?;
4041

41-
let mut out = Vec::new();
42+
let mut out = Vec::new();
4243

43-
for s in sections {
44-
let mut custom_roms = None;
45-
if s.section_type == "custom" {
46-
let rom_ids = sqlx::query!(
44+
for s in sections {
45+
let mut custom_roms = None;
46+
if s.section_type == "custom" {
47+
let rom_ids = sqlx::query!(
4748
"SELECT rom_id FROM search_section_roms WHERE section_id = $1 ORDER BY order_index ASC",
4849
s.id
4950
)
@@ -53,21 +54,21 @@ pub async fn get_search_sections(
5354
error!("Failed to fetch custom section roms: {:?}", e);
5455
V1ApiError::InternalError
5556
})?;
56-
custom_roms = Some(rom_ids.into_iter().map(|r| r.rom_id).collect());
57-
}
58-
59-
out.push(V1SearchSectionResponse {
60-
id: s.id,
61-
title: s.title,
62-
section_type: s.section_type,
63-
smart_filter: s.smart_filter,
64-
filter_value: s.filter_value,
65-
order_index: s.order_index,
66-
roms: custom_roms,
67-
});
57+
custom_roms = Some(rom_ids.into_iter().map(|r| r.rom_id).collect());
6858
}
6959

70-
Ok(V1ApiResponse(out))
60+
out.push(V1SearchSectionResponse {
61+
id: Id::new(s.id),
62+
title: s.title,
63+
section_type: s.section_type,
64+
smart_filter: s.smart_filter,
65+
filter_value: s.filter_value,
66+
order_index: s.order_index,
67+
roms: custom_roms,
68+
});
69+
}
70+
71+
Ok(V1ApiResponse(out))
7172
}
7273

7374
#[derive(Deserialize)]
@@ -210,7 +211,10 @@ pub async fn update_search_section(
210211
.execute(&mut *tx)
211212
.await
212213
.map_err(|e| {
213-
error!("Failed to clear section roms (type changed from custom): {:?}", e);
214+
error!(
215+
"Failed to clear section roms (type changed from custom): {:?}",
216+
e
217+
);
214218
V1ApiError::InternalError
215219
})?;
216220
}
@@ -225,8 +229,7 @@ pub async fn update_search_section(
225229

226230
#[derive(Deserialize)]
227231
pub struct V1SearchSectionOrderUpdateRequest {
228-
// Array of (id, order_index)
229-
pub updates: Vec<(i64, i32)>,
232+
pub updates: Vec<(Id, i32)>,
230233
}
231234

232235
#[put("/api/v1/search_sections/order", format = "json", data = "<data>")]
@@ -237,20 +240,24 @@ pub async fn update_search_sections_order(
237240
if user.role != UserRole::Admin && user.role != UserRole::Moderator {
238241
return Err(V1ApiError::NotAuthorized);
239242
}
240-
243+
241244
let mut tx = SQL.begin().await.map_err(|e| {
242245
error!("Failed to begin transaction: {:?}", e);
243246
V1ApiError::InternalError
244247
})?;
245248

246249
for (id, order) in &data.updates {
247-
sqlx::query!("UPDATE search_sections SET order_index = $1 WHERE id = $2", order, id)
248-
.execute(&mut *tx)
249-
.await
250-
.map_err(|e| {
251-
error!("Failed to update order for {}: {:?}", id, e);
252-
V1ApiError::InternalError
253-
})?;
250+
sqlx::query!(
251+
"UPDATE search_sections SET order_index = $1 WHERE id = $2",
252+
order,
253+
id.value()
254+
)
255+
.execute(&mut *tx)
256+
.await
257+
.map_err(|e| {
258+
error!("Failed to update order for {}: {:?}", id, e);
259+
V1ApiError::InternalError
260+
})?;
254261
}
255262

256263
tx.commit().await.map_err(|e| {
@@ -262,10 +269,7 @@ pub async fn update_search_sections_order(
262269
}
263270

264271
#[delete("/api/v1/search_sections/<id>")]
265-
pub async fn delete_search_section(
266-
id: i64,
267-
user: AuthenticatedUser,
268-
) -> V1ApiResponseType<()> {
272+
pub async fn delete_search_section(id: i64, user: AuthenticatedUser) -> V1ApiResponseType<()> {
269273
if user.role != UserRole::Admin && user.role != UserRole::Moderator {
270274
return Err(V1ApiError::NotAuthorized);
271275
}

src/utils/migration.rs

Lines changed: 105 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,46 @@ use std::io::Cursor;
66
pub async fn migrate_covers() {
77
println!("Checking if any covers need migration...");
88

9+
let mut root_covers = std::collections::HashMap::new();
10+
let mut root_smalls = std::collections::HashMap::new();
11+
let mut root_icons = std::collections::HashMap::new();
12+
13+
let bucket = &*crate::S3;
14+
15+
for base in ["covers", "covers_small", "icons"] {
16+
for prefix in [format!("{}/", base), format!("/{}/", base)] {
17+
if let Ok(results) = bucket.list(prefix.clone(), Some("/".to_string())).await {
18+
let mut count = 0;
19+
20+
for res in results {
21+
for obj in res.contents {
22+
if obj.key.ends_with('/') {
23+
continue;
24+
}
25+
26+
let path = std::path::Path::new(&obj.key);
27+
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
28+
let stem = stem.to_string();
29+
if base == "covers" {
30+
root_covers.insert(stem, obj.key);
31+
} else if base == "covers_small" {
32+
root_smalls.insert(stem, obj.key);
33+
} else if base == "icons" {
34+
root_icons.insert(stem, obj.key);
35+
}
36+
count += 1;
37+
}
38+
}
39+
}
40+
}
41+
}
42+
}
43+
44+
println!(
45+
"Total images sitting in root folders: {}",
46+
root_covers.len() + root_smalls.len() + root_icons.len()
47+
);
48+
949
let roms = match sqlx::query!("SELECT id, console, image_hash FROM roms")
1050
.fetch_all(&*SQL)
1151
.await
@@ -23,20 +63,26 @@ pub async fn migrate_covers() {
2363
let hash = &rom.image_hash;
2464

2565
let new_cover = format!("covers/{}/{}/{}.webp", rom.console, rom.id, hash);
26-
let old_cover = format!("covers/{}.webp", hash);
27-
2866
let new_small = format!("covers_small/{}/{}/{}.webp", rom.console, rom.id, hash);
29-
let old_small = format!("covers_small/{}.webp", hash);
30-
3167
let new_icon = format!("icons/{}/{}/{}.webp", rom.console, rom.id, hash);
32-
let old_icon = format!("icons/{}.webp", hash);
68+
69+
let old_cover_key = root_covers.get(hash);
70+
if old_cover_key.is_none() {
71+
if crate::utils::s3::download_object(&new_cover).await.is_ok() {
72+
continue;
73+
}
74+
}
3375

3476
let mut moved_any = false;
3577
let mut source_bytes = None;
3678

37-
if let Ok(bytes) = crate::utils::s3::download_object(&old_cover).await {
38-
source_bytes = Some(bytes);
39-
} else {
79+
if let Some(key) = old_cover_key {
80+
if let Ok(bytes) = crate::utils::s3::download_object(key).await {
81+
source_bytes = Some(bytes);
82+
}
83+
}
84+
85+
if source_bytes.is_none() {
4086
let shared_roms = sqlx::query!(
4187
"SELECT id, console FROM roms WHERE image_hash = $1 AND id != $2",
4288
rom.image_hash,
@@ -52,45 +98,67 @@ pub async fn migrate_covers() {
5298
if let Ok(bytes) = crate::utils::s3::download_object(&other_new_path).await {
5399
source_bytes = Some(bytes);
54100

55-
// Also try to recover small and icon from the same source ROM if needed
56-
if crate::utils::s3::download_object(&old_small).await.is_err() {
101+
if crate::utils::s3::download_object(&new_small).await.is_err() {
57102
let other_new_small = format!(
58103
"covers_small/{}/{}/{}.webp",
59104
s.console, s.id, rom.image_hash
60105
);
61-
if let Ok(s_bytes) =
62-
crate::utils::s3::download_object(&other_new_small).await
63-
{
64-
let _ = crate::utils::s3::upload_object(&new_small, &s_bytes).await;
106+
if let Some(key) = root_smalls.get(hash) {
107+
if let Ok(s_bytes) = crate::utils::s3::download_object(key).await {
108+
let _ = crate::utils::s3::upload_object(&new_small, &s_bytes).await;
109+
}
65110
}
66111
}
67-
if crate::utils::s3::download_object(&old_icon).await.is_err() {
112+
if crate::utils::s3::download_object(&new_icon).await.is_err() {
68113
let other_new_icon =
69114
format!("icons/{}/{}/{}.webp", s.console, s.id, rom.image_hash);
70-
if let Ok(i_bytes) =
71-
crate::utils::s3::download_object(&other_new_icon).await
72-
{
73-
let _ = crate::utils::s3::upload_object(&new_icon, &i_bytes).await;
115+
if let Some(key) = root_icons.get(hash) {
116+
if let Ok(i_bytes) = crate::utils::s3::download_object(key).await {
117+
let _ = crate::utils::s3::upload_object(&new_icon, &i_bytes).await;
118+
}
74119
}
75120
}
76121
break;
77122
}
78123
}
79124
}
80125

81-
if let Some(bytes) = source_bytes {
126+
if let Some(mut bytes) = source_bytes {
127+
if let Ok(img) = image::load_from_memory(&bytes) {
128+
let mut buf = std::io::Cursor::new(Vec::new());
129+
if img.write_to(&mut buf, image::ImageFormat::WebP).is_ok() {
130+
bytes = buf.into_inner();
131+
}
132+
}
133+
82134
if crate::utils::s3::upload_object(&new_cover, &bytes)
83135
.await
84136
.is_ok()
85137
{
86138
moved_any = true;
87139
}
88140

89-
if let Ok(s_bytes) = crate::utils::s3::download_object(&old_small).await {
90-
let _ = crate::utils::s3::upload_object(&new_small, &s_bytes).await;
141+
if let Some(key) = root_smalls.get(hash) {
142+
if let Ok(mut s_bytes) = crate::utils::s3::download_object(key).await {
143+
if let Ok(img) = image::load_from_memory(&s_bytes) {
144+
let mut buf = std::io::Cursor::new(Vec::new());
145+
if img.write_to(&mut buf, image::ImageFormat::WebP).is_ok() {
146+
s_bytes = buf.into_inner();
147+
}
148+
}
149+
let _ = crate::utils::s3::upload_object(&new_small, &s_bytes).await;
150+
}
91151
}
92-
if let Ok(i_bytes) = crate::utils::s3::download_object(&old_icon).await {
93-
let _ = crate::utils::s3::upload_object(&new_icon, &i_bytes).await;
152+
if let Some(key) = root_icons.get(hash) {
153+
if let Ok(mut i_bytes) = crate::utils::s3::download_object(key).await {
154+
if let Ok(img) = image::load_from_memory(&i_bytes) {
155+
let mut buf = std::io::Cursor::new(Vec::new());
156+
if img.write_to(&mut buf, image::ImageFormat::WebP).is_ok() {
157+
i_bytes = buf.into_inner();
158+
}
159+
}
160+
let _ = crate::utils::s3::upload_object(&new_icon, &i_bytes).await;
161+
}
94162
}
95163
}
96164

@@ -107,12 +175,19 @@ pub async fn migrate_covers() {
107175
}
108176

109177
println!("Cleaning up unused covers in root directories...");
110-
for prefix in ["covers/", "covers_small/", "icons/"] {
111-
if let Ok(results) = crate::utils::s3::list_objects_shallow(prefix).await {
112-
for key in results {
113-
if key.ends_with(".webp") {
114-
println!("Deleting unused file: {}", key);
115-
let _ = crate::utils::s3::delete_object(&key).await;
178+
179+
for base in ["covers", "covers_small", "icons"] {
180+
for prefix in [format!("{}/", base), format!("/{}/", base)] {
181+
let bucket = &*crate::S3;
182+
183+
if let Ok(results) = bucket.list(prefix, Some("/".to_string())).await {
184+
for res in results {
185+
for obj in res.contents {
186+
if !obj.key.ends_with('/') {
187+
println!("Deleting unused file: {}", obj.key);
188+
let _ = bucket.delete_object(&obj.key).await;
189+
}
190+
}
116191
}
117192
}
118193
}

0 commit comments

Comments
 (0)