feat(service): expand service layer with new domain operations
- Add IM service modules: audit, channel roles, custom emojis, forum tags, integrations, invitations, repo links, slash commands, stages, voice, webhooks - Add PR service modules: review requests, templates - Add repo service modules: contributors, release assets, git extras (archive, branch rename, commit extras, diff/merge, tag, tree) - Add user service: social (follow/block) - Add internal auth service - Update existing service modules with expanded functionality - Remove deleted IM modules: articles, delivery trace, drafts, follows, messages, polls, presence, reactions, threads
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
use crate::error::AppError;
|
||||
use crate::models::repos::{RepoRelease, RepoReleaseAsset};
|
||||
use crate::service::RepoService;
|
||||
use crate::session::Session;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::util::clamp_limit_offset;
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
|
||||
pub struct ReleaseAssetData {
|
||||
pub id: Uuid,
|
||||
pub filename: String,
|
||||
pub size_bytes: i64,
|
||||
pub mime_type: String,
|
||||
pub download_count: i64,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl From<RepoReleaseAsset> for ReleaseAssetData {
|
||||
fn from(a: RepoReleaseAsset) -> Self {
|
||||
Self {
|
||||
id: a.id,
|
||||
filename: a.filename,
|
||||
size_bytes: a.size_bytes,
|
||||
mime_type: a.mime_type,
|
||||
download_count: a.download_count,
|
||||
created_at: a.created_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RepoService {
|
||||
pub async fn repo_upload_release_asset(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
release_id: Uuid,
|
||||
filename: &str,
|
||||
data: Vec<u8>,
|
||||
content_type: &str,
|
||||
) -> Result<ReleaseAssetData, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_role_at_least(user_uid, &repo, crate::models::common::Role::Member)
|
||||
.await?;
|
||||
|
||||
let release: Option<RepoRelease> = sqlx::query_as(
|
||||
"SELECT * FROM repo_release WHERE id = $1 AND repo_id = $2 AND deleted_at IS NULL",
|
||||
)
|
||||
.bind(release_id)
|
||||
.bind(repo.id)
|
||||
.fetch_optional(self.ctx.db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
if release.is_none() {
|
||||
return Err(AppError::NotFound("release not found".into()));
|
||||
}
|
||||
|
||||
let asset_id = Uuid::now_v7();
|
||||
let storage_key = format!("repos/{}/releases/{}/{}", repo.id, release_id, asset_id);
|
||||
let size = data.len() as i64;
|
||||
self.ctx.storage.put(&storage_key, data).await?;
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let asset = sqlx::query_as::<_, RepoReleaseAsset>(
|
||||
"INSERT INTO repo_release_asset (id, release_id, filename, size_bytes, mime_type, storage_path, uploaded_by, created_at, updated_at) \
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $8) \
|
||||
RETURNING *",
|
||||
)
|
||||
.bind(asset_id)
|
||||
.bind(release_id)
|
||||
.bind(filename)
|
||||
.bind(size)
|
||||
.bind(content_type)
|
||||
.bind(&storage_key)
|
||||
.bind(user_uid)
|
||||
.bind(now)
|
||||
.fetch_one(self.ctx.db.writer())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let storage_key = storage_key.clone();
|
||||
let storage = self.ctx.storage.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = storage.delete(&storage_key).await;
|
||||
});
|
||||
AppError::Database(e)
|
||||
})?;
|
||||
|
||||
Ok(ReleaseAssetData::from(asset))
|
||||
}
|
||||
|
||||
pub async fn repo_list_release_assets(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
release_id: Uuid,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
) -> Result<Vec<ReleaseAssetData>, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
|
||||
let (limit, offset) = clamp_limit_offset(limit, offset);
|
||||
let assets = sqlx::query_as::<_, RepoReleaseAsset>(
|
||||
"SELECT * FROM repo_release_asset WHERE release_id = $1 AND deleted_at IS NULL ORDER BY created_at ASC LIMIT $2 OFFSET $3",
|
||||
)
|
||||
.bind(release_id)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(self.ctx.db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
Ok(assets.into_iter().map(ReleaseAssetData::from).collect())
|
||||
}
|
||||
|
||||
pub async fn repo_delete_release_asset(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
release_id: Uuid,
|
||||
asset_id: Uuid,
|
||||
) -> Result<(), AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_role_at_least(user_uid, &repo, crate::models::common::Role::Admin)
|
||||
.await?;
|
||||
|
||||
let asset: Option<RepoReleaseAsset> = sqlx::query_as(
|
||||
"SELECT * FROM repo_release_asset WHERE id = $1 AND release_id = $2 AND deleted_at IS NULL",
|
||||
)
|
||||
.bind(asset_id)
|
||||
.bind(release_id)
|
||||
.fetch_optional(self.ctx.db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
let Some(asset) = asset else {
|
||||
return Err(AppError::NotFound("asset not found".into()));
|
||||
};
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
sqlx::query("UPDATE repo_release_asset SET deleted_at = $1, updated_at = $1 WHERE id = $2")
|
||||
.bind(now)
|
||||
.bind(asset_id)
|
||||
.execute(self.ctx.db.writer())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
let _ = self.ctx.storage.delete(&asset.storage_path).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn repo_get_release_asset_download_url(
|
||||
&self,
|
||||
ctx: &Session,
|
||||
wk_name: &str,
|
||||
repo_name: &str,
|
||||
release_id: Uuid,
|
||||
asset_id: Uuid,
|
||||
) -> Result<String, AppError> {
|
||||
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
|
||||
let repo = self.resolve_repo(wk_name, repo_name).await?;
|
||||
self.ensure_repo_readable(user_uid, &repo).await?;
|
||||
|
||||
let asset: Option<RepoReleaseAsset> = sqlx::query_as(
|
||||
"SELECT * FROM repo_release_asset WHERE id = $1 AND release_id = $2 AND deleted_at IS NULL",
|
||||
)
|
||||
.bind(asset_id)
|
||||
.bind(release_id)
|
||||
.fetch_optional(self.ctx.db.reader())
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
|
||||
let Some(asset) = asset else {
|
||||
return Err(AppError::NotFound("asset not found".into()));
|
||||
};
|
||||
|
||||
if let Some(url) = asset.url {
|
||||
return Ok(url);
|
||||
}
|
||||
|
||||
let url = self
|
||||
.ctx
|
||||
.storage
|
||||
.presigned_get_url(&asset.storage_path, None)
|
||||
.await?;
|
||||
|
||||
// Update download count and cache the URL
|
||||
let _ = sqlx::query(
|
||||
"UPDATE repo_release_asset SET download_count = download_count + 1, url = $1, updated_at = NOW() WHERE id = $2",
|
||||
)
|
||||
.bind(&url)
|
||||
.bind(asset_id)
|
||||
.execute(self.ctx.db.writer())
|
||||
.await;
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user