420dedbc1e
- 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
206 lines
6.5 KiB
Rust
206 lines
6.5 KiB
Rust
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)
|
|
}
|
|
}
|