e8e4ef4b15
Five benchmark-gated optimizations from a fresh six-way audit (benches/ROUND25.md), each with a BEFORE/AFTER gate that rolls back if AFTER does not beat BEFORE: - M1 EncryptedBlobBackend::decrypt_bytes: replace split_off (a fresh Vec + full ciphertext memcpy on every decrypted chunk, contradicting its own "in place" doc) with in-place detached decrypt + a zero-copy Bytes::slice past the nonce. Peak RAM per read drops from ~2x to ~1x the payload (-262KB/op at 256KiB; scales with blob size). Plaintext byte-identical; tamper/wrong-key tests pass. - M2 delta commit: move-unzip the owned chunk list instead of a third per-occurrence hash clone (-4000 allocs on a 4000-chunk commit). - M3 folder ZIP download: drop the dead Query<HashMap> extractor it never read (byte-identical response; 5->0 allocs/request). - Q1 public-playlist listing: fold the per-playlist COUNT(*) N+1 into one LEFT JOIN ... GROUP BY via a new inherent repo method (101 -> 1 round-trips, 36x wall on a 100-playlist page). - Q2 contact REST listings (paginated/search/by-group): stop over-fetching the multi-KB vcard TEXT the ContactDto discards, via a shared lite row mapper and narrowed SELECTs (6.4x wall on 1000 contacts with 8KiB vcards). The whole-book vCard export and CardDAV sync paths keep the column. Adds bench_round25_micro (counting allocator tracking count+bytes) and bench_round25_queries (live Postgres), both with equivalence gates and a rollback exit(1). Verified: cargo fmt clean, cargo clippy -D warnings clean, cargo test --lib --features bench = 529 passed / 0 failed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L8gs91AhmazoxMsDcNk3KT
301 lines
10 KiB
Rust
301 lines
10 KiB
Rust
use std::sync::Arc;
|
|
use uuid::Uuid;
|
|
|
|
use crate::application::dtos::playlist_dto::{
|
|
AudioMetadataDto, CreatePlaylistDto, PlaylistDto, PlaylistItemDto, UpdatePlaylistDto,
|
|
};
|
|
use crate::application::ports::music_ports::MusicStoragePort;
|
|
use crate::common::errors::{DomainError, ErrorKind};
|
|
use crate::domain::entities::playlist::Playlist;
|
|
use crate::domain::repositories::playlist_repository::{
|
|
AudioMetadataRepository, PlaylistItemRepository, PlaylistRepository,
|
|
};
|
|
use crate::infrastructure::repositories::pg::{
|
|
AudioMetadataPgRepository, PlaylistItemPgRepository, PlaylistPgRepository,
|
|
};
|
|
|
|
pub struct MusicStorageAdapter {
|
|
playlist_repository: Arc<PlaylistPgRepository>,
|
|
item_repository: Arc<PlaylistItemPgRepository>,
|
|
audio_metadata_repository: Arc<AudioMetadataPgRepository>,
|
|
}
|
|
|
|
impl MusicStorageAdapter {
|
|
pub fn new(
|
|
playlist_repository: Arc<PlaylistPgRepository>,
|
|
item_repository: Arc<PlaylistItemPgRepository>,
|
|
audio_metadata_repository: Arc<AudioMetadataPgRepository>,
|
|
) -> Self {
|
|
Self {
|
|
playlist_repository,
|
|
item_repository,
|
|
audio_metadata_repository,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl MusicStoragePort for MusicStorageAdapter {
|
|
async fn create_playlist(
|
|
&self,
|
|
dto: CreatePlaylistDto,
|
|
user_id: Uuid,
|
|
) -> Result<PlaylistDto, DomainError> {
|
|
let playlist = Playlist::new(dto.name, user_id, dto.description)?;
|
|
let created = self.playlist_repository.create_playlist(playlist).await?;
|
|
Ok(PlaylistDto::from(created))
|
|
}
|
|
|
|
async fn update_playlist(
|
|
&self,
|
|
playlist_id: &str,
|
|
dto: UpdatePlaylistDto,
|
|
) -> Result<PlaylistDto, DomainError> {
|
|
let uuid = Uuid::parse_str(playlist_id).map_err(|_| {
|
|
DomainError::new(ErrorKind::InvalidInput, "Playlist", "Invalid playlist ID")
|
|
})?;
|
|
|
|
let mut playlist = self.playlist_repository.find_playlist_by_id(&uuid).await?;
|
|
|
|
if let Some(name) = dto.name {
|
|
playlist.update_name(name)?;
|
|
}
|
|
if let Some(description) = dto.description {
|
|
playlist.update_description(Some(description));
|
|
}
|
|
if let Some(is_public) = dto.is_public {
|
|
playlist.set_public(is_public);
|
|
}
|
|
if let Some(cover_file_id) = dto.cover_file_id {
|
|
let cover_uuid = Uuid::parse_str(&cover_file_id).map_err(|_| {
|
|
DomainError::new(ErrorKind::InvalidInput, "Playlist", "Invalid cover file ID")
|
|
})?;
|
|
playlist.set_cover(Some(cover_uuid));
|
|
}
|
|
|
|
let updated = self.playlist_repository.update_playlist(playlist).await?;
|
|
Ok(PlaylistDto::from(updated))
|
|
}
|
|
|
|
async fn delete_playlist(&self, playlist_id: &str) -> Result<(), DomainError> {
|
|
let uuid = Uuid::parse_str(playlist_id).map_err(|_| {
|
|
DomainError::new(ErrorKind::InvalidInput, "Playlist", "Invalid playlist ID")
|
|
})?;
|
|
self.playlist_repository.delete_playlist(&uuid).await
|
|
}
|
|
|
|
async fn get_playlist(&self, playlist_id: &str) -> Result<Option<PlaylistDto>, DomainError> {
|
|
let uuid = Uuid::parse_str(playlist_id).map_err(|_| {
|
|
DomainError::new(ErrorKind::InvalidInput, "Playlist", "Invalid playlist ID")
|
|
})?;
|
|
|
|
match self.playlist_repository.find_playlist_by_id(&uuid).await {
|
|
Ok(playlist) => Ok(Some(PlaylistDto::from(playlist))),
|
|
Err(e) if e.kind == ErrorKind::NotFound => Ok(None),
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
|
|
async fn get_playlists_by_ids(&self, ids: &[Uuid]) -> Result<Vec<PlaylistDto>, DomainError> {
|
|
let playlists = self.playlist_repository.find_playlists_by_ids(ids).await?;
|
|
Ok(playlists.into_iter().map(PlaylistDto::from).collect())
|
|
}
|
|
|
|
async fn list_playlists_by_owner(
|
|
&self,
|
|
owner_id: Uuid,
|
|
) -> Result<Vec<PlaylistDto>, DomainError> {
|
|
let playlists = self
|
|
.playlist_repository
|
|
.list_playlists_by_owner(owner_id)
|
|
.await?;
|
|
let mut result = Vec::new();
|
|
for playlist in playlists {
|
|
let dto = PlaylistDto::from(playlist);
|
|
let track_count = self
|
|
.get_track_count(&uuid::Uuid::parse_str(&dto.id).unwrap())
|
|
.await?;
|
|
result.push(dto.with_track_info(track_count, 0));
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
async fn list_shared_with_user(&self, user_id: Uuid) -> Result<Vec<PlaylistDto>, DomainError> {
|
|
let playlists = self
|
|
.playlist_repository
|
|
.list_shared_with_user(user_id)
|
|
.await?;
|
|
let mut result = Vec::new();
|
|
for playlist in playlists {
|
|
let dto = PlaylistDto::from(playlist);
|
|
let track_count = self
|
|
.get_track_count(&uuid::Uuid::parse_str(&dto.id).unwrap())
|
|
.await?;
|
|
result.push(dto.with_track_info(track_count, 0));
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
async fn list_public_playlists(
|
|
&self,
|
|
limit: i64,
|
|
offset: i64,
|
|
) -> Result<Vec<PlaylistDto>, DomainError> {
|
|
// One `LEFT JOIN … GROUP BY` instead of 1 listing + N per-playlist
|
|
// `COUNT(*)` round-trips (up to 101 at limit=100) — benches/ROUND25.md §Q1.
|
|
let playlists = self
|
|
.playlist_repository
|
|
.list_public_playlists_with_counts(limit, offset)
|
|
.await?;
|
|
Ok(playlists
|
|
.into_iter()
|
|
.map(|(playlist, track_count)| {
|
|
PlaylistDto::from(playlist).with_track_info(track_count, 0)
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
async fn user_has_access(&self, playlist_id: &str, user_id: Uuid) -> Result<bool, DomainError> {
|
|
let uuid = Uuid::parse_str(playlist_id).map_err(|_| {
|
|
DomainError::new(ErrorKind::InvalidInput, "Playlist", "Invalid playlist ID")
|
|
})?;
|
|
self.playlist_repository
|
|
.user_has_access(&uuid, user_id)
|
|
.await
|
|
}
|
|
|
|
async fn user_can_write(&self, playlist_id: &str, user_id: Uuid) -> Result<bool, DomainError> {
|
|
let uuid = Uuid::parse_str(playlist_id).map_err(|_| {
|
|
DomainError::new(ErrorKind::InvalidInput, "Playlist", "Invalid playlist ID")
|
|
})?;
|
|
|
|
let playlist = self.playlist_repository.find_playlist_by_id(&uuid).await?;
|
|
if playlist.owner_id() == &user_id {
|
|
return Ok(true);
|
|
}
|
|
|
|
let shares = self.playlist_repository.get_shares(&uuid).await?;
|
|
Ok(shares
|
|
.iter()
|
|
.any(|(uid, can_write)| uid == &user_id && *can_write))
|
|
}
|
|
|
|
async fn add_tracks(
|
|
&self,
|
|
playlist_id: &Uuid,
|
|
file_ids: &[Uuid],
|
|
) -> Result<Vec<PlaylistItemDto>, DomainError> {
|
|
let mut max_position = self.item_repository.get_max_position(playlist_id).await?;
|
|
let mut items = Vec::new();
|
|
|
|
for file_id in file_ids {
|
|
max_position += 1;
|
|
let item = crate::domain::entities::playlist::PlaylistItem::new(
|
|
*playlist_id,
|
|
*file_id,
|
|
max_position,
|
|
)?;
|
|
let created = self.item_repository.add_item(item).await?;
|
|
items.push(PlaylistItemDto::from(created));
|
|
}
|
|
|
|
Ok(items)
|
|
}
|
|
|
|
async fn remove_track(&self, playlist_id: &Uuid, file_id: &Uuid) -> Result<(), DomainError> {
|
|
self.item_repository
|
|
.remove_item_by_playlist_and_file(playlist_id, file_id)
|
|
.await
|
|
}
|
|
|
|
async fn reorder_tracks(
|
|
&self,
|
|
playlist_id: &Uuid,
|
|
item_ids: &[Uuid],
|
|
) -> Result<(), DomainError> {
|
|
self.item_repository
|
|
.reorder_items(playlist_id, item_ids)
|
|
.await
|
|
}
|
|
|
|
async fn list_playlist_tracks(
|
|
&self,
|
|
playlist_id: &Uuid,
|
|
) -> Result<Vec<PlaylistItemDto>, DomainError> {
|
|
let enriched_items = self
|
|
.item_repository
|
|
.list_items_in_playlist_enriched(playlist_id)
|
|
.await?;
|
|
|
|
let items: Vec<PlaylistItemDto> = enriched_items
|
|
.into_iter()
|
|
.map(
|
|
|row| crate::application::dtos::playlist_dto::PlaylistItemDto {
|
|
id: row.id.to_string(),
|
|
playlist_id: row.playlist_id.to_string(),
|
|
file_id: row.file_id.to_string(),
|
|
position: row.position,
|
|
added_at: row.added_at,
|
|
file_name: row.file_name,
|
|
file_size: row.file_size,
|
|
mime_type: row.mime_type,
|
|
title: row.title,
|
|
artist: row.artist,
|
|
album: row.album,
|
|
duration_secs: row.duration_secs,
|
|
},
|
|
)
|
|
.collect();
|
|
Ok(items)
|
|
}
|
|
|
|
async fn share_playlist(
|
|
&self,
|
|
playlist_id: &Uuid,
|
|
user_id: Uuid,
|
|
can_write: bool,
|
|
) -> Result<(), DomainError> {
|
|
self.playlist_repository
|
|
.share_playlist(playlist_id, user_id, can_write)
|
|
.await
|
|
}
|
|
|
|
async fn remove_share(&self, playlist_id: &Uuid, user_id: Uuid) -> Result<(), DomainError> {
|
|
self.playlist_repository
|
|
.remove_share(playlist_id, user_id)
|
|
.await
|
|
}
|
|
|
|
async fn get_shares(&self, playlist_id: &Uuid) -> Result<Vec<(Uuid, bool)>, DomainError> {
|
|
self.playlist_repository.get_shares(playlist_id).await
|
|
}
|
|
|
|
async fn get_audio_metadata(
|
|
&self,
|
|
file_id: &Uuid,
|
|
) -> Result<Option<AudioMetadataDto>, DomainError> {
|
|
match self
|
|
.audio_metadata_repository
|
|
.find_by_file_id(file_id)
|
|
.await
|
|
{
|
|
Ok(Some(metadata)) => Ok(Some(AudioMetadataDto::from(metadata))),
|
|
Ok(None) => Ok(None),
|
|
Err(e) => Err(e),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl MusicStorageAdapter {
|
|
async fn get_track_count(&self, playlist_id: &Uuid) -> Result<i64, DomainError> {
|
|
let count: (i64,) =
|
|
sqlx::query_as("SELECT COUNT(*) FROM audio.playlist_items WHERE playlist_id = $1")
|
|
.bind(playlist_id)
|
|
.fetch_one(self.playlist_repository.pool())
|
|
.await
|
|
.map_err(|e| {
|
|
DomainError::database_error(format!("Failed to get track count: {}", e))
|
|
})?;
|
|
Ok(count.0)
|
|
}
|
|
}
|