448 lines
14 KiB
Rust
448 lines
14 KiB
Rust
use chrono::Utc;
|
|
use db::sqlx;
|
|
use model::repos::{RepoReleaseAssetModel, RepoReleaseModel};
|
|
use session::Session;
|
|
use uuid::Uuid;
|
|
|
|
use crate::AppService;
|
|
use crate::error::AppError;
|
|
|
|
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
|
|
pub struct ReleaseResponse {
|
|
pub id: Uuid,
|
|
pub tag_name: String,
|
|
pub target_commit_sha: String,
|
|
pub name: String,
|
|
pub body: Option<String>,
|
|
pub draft: bool,
|
|
pub prerelease: bool,
|
|
pub author: Uuid,
|
|
pub assets: Vec<ReleaseAssetResponse>,
|
|
pub published_at: Option<chrono::DateTime<Utc>>,
|
|
pub created_at: chrono::DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)]
|
|
pub struct ReleaseAssetResponse {
|
|
pub id: Uuid,
|
|
pub name: String,
|
|
pub content_type: Option<String>,
|
|
pub size: i64,
|
|
pub download_count: i64,
|
|
pub created_at: chrono::DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)]
|
|
pub struct CreateRelease {
|
|
pub tag_name: String,
|
|
pub target_commit_sha: Option<String>,
|
|
pub name: String,
|
|
pub body: Option<String>,
|
|
#[serde(default)]
|
|
pub draft: bool,
|
|
#[serde(default)]
|
|
pub prerelease: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Deserialize, utoipa::ToSchema)]
|
|
pub struct UpdateRelease {
|
|
pub tag_name: Option<String>,
|
|
pub name: Option<String>,
|
|
pub body: Option<Option<String>>,
|
|
pub draft: Option<bool>,
|
|
pub prerelease: Option<bool>,
|
|
}
|
|
|
|
impl AppService {
|
|
pub async fn git_release_list(
|
|
&self,
|
|
repo_id: Uuid,
|
|
) -> Result<Vec<ReleaseResponse>, AppError> {
|
|
let releases = sqlx::query_as::<_, RepoReleaseModel>(
|
|
"SELECT id, repo, tag_name, target_commit_sha, name, body, \
|
|
draft, prerelease, author, published_at, created_at, updated_at \
|
|
FROM repo_release WHERE repo = $1 ORDER BY created_at DESC LIMIT 50",
|
|
)
|
|
.bind(repo_id)
|
|
.fetch_all(self.db.reader())
|
|
.await
|
|
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
|
|
|
|
let mut result = Vec::with_capacity(releases.len());
|
|
for r in releases {
|
|
let assets = self.git_release_assets(r.id).await?;
|
|
result.push(release_to_response(r, assets));
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
pub async fn git_release_get(
|
|
&self,
|
|
repo_id: Uuid,
|
|
release_id: Uuid,
|
|
) -> Result<ReleaseResponse, AppError> {
|
|
let r = sqlx::query_as::<_, RepoReleaseModel>(
|
|
"SELECT id, repo, tag_name, target_commit_sha, name, body, \
|
|
draft, prerelease, author, published_at, created_at, updated_at \
|
|
FROM repo_release WHERE id = $1 AND repo = $2",
|
|
)
|
|
.bind(release_id)
|
|
.bind(repo_id)
|
|
.fetch_optional(self.db.reader())
|
|
.await
|
|
.map_err(|e| AppError::DatabaseError(e.to_string()))?
|
|
.ok_or_else(|| AppError::NotFound("release not found".to_string()))?;
|
|
|
|
let assets = self.git_release_assets(r.id).await?;
|
|
Ok(release_to_response(r, assets))
|
|
}
|
|
|
|
pub async fn git_release_get_by_tag(
|
|
&self,
|
|
repo_id: Uuid,
|
|
tag: &str,
|
|
) -> Result<ReleaseResponse, AppError> {
|
|
let r = sqlx::query_as::<_, RepoReleaseModel>(
|
|
"SELECT id, repo, tag_name, target_commit_sha, name, body, \
|
|
draft, prerelease, author, published_at, created_at, updated_at \
|
|
FROM repo_release WHERE repo = $1 AND tag_name = $2",
|
|
)
|
|
.bind(repo_id)
|
|
.bind(tag)
|
|
.fetch_optional(self.db.reader())
|
|
.await
|
|
.map_err(|e| AppError::DatabaseError(e.to_string()))?
|
|
.ok_or_else(|| AppError::NotFound("release not found".to_string()))?;
|
|
|
|
let assets = self.git_release_assets(r.id).await?;
|
|
Ok(release_to_response(r, assets))
|
|
}
|
|
|
|
pub async fn git_release_create(
|
|
&self,
|
|
_session: &Session,
|
|
repo_id: Uuid,
|
|
user_id: Uuid,
|
|
params: CreateRelease,
|
|
) -> Result<ReleaseResponse, AppError> {
|
|
let id = Uuid::now_v7();
|
|
let now = Utc::now();
|
|
let published_at = if params.draft { None } else { Some(now) };
|
|
|
|
let target = if let Some(ref sha) = params.target_commit_sha {
|
|
if !sha.trim().is_empty() {
|
|
sha.clone()
|
|
} else {
|
|
self.default_branch_sha(repo_id).await?
|
|
}
|
|
} else {
|
|
self.default_branch_sha(repo_id).await?
|
|
};
|
|
|
|
let r = sqlx::query_as::<_, RepoReleaseModel>(
|
|
"INSERT INTO repo_release (id, repo, tag_name, target_commit_sha, name, body, \
|
|
draft, prerelease, author, published_at, created_at, updated_at) \
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$11) \
|
|
RETURNING id, repo, tag_name, target_commit_sha, name, body, \
|
|
draft, prerelease, author, published_at, created_at, updated_at",
|
|
)
|
|
.bind(id)
|
|
.bind(repo_id)
|
|
.bind(¶ms.tag_name)
|
|
.bind(&target)
|
|
.bind(¶ms.name)
|
|
.bind(¶ms.body)
|
|
.bind(params.draft)
|
|
.bind(params.prerelease)
|
|
.bind(user_id)
|
|
.bind(published_at)
|
|
.bind(now)
|
|
.fetch_one(self.db.writer())
|
|
.await
|
|
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
|
|
|
|
Ok(release_to_response(r, Vec::new()))
|
|
}
|
|
|
|
pub async fn git_release_update(
|
|
&self,
|
|
repo_id: Uuid,
|
|
release_id: Uuid,
|
|
params: UpdateRelease,
|
|
) -> Result<ReleaseResponse, AppError> {
|
|
let existing = self.git_release_get(repo_id, release_id).await?;
|
|
let now = Utc::now();
|
|
|
|
let tag_name = params.tag_name.unwrap_or(existing.tag_name);
|
|
let name = params.name.unwrap_or(existing.name);
|
|
let body = params.body.unwrap_or(existing.body);
|
|
let draft = params.draft.unwrap_or(existing.draft);
|
|
let prerelease = params.prerelease.unwrap_or(existing.prerelease);
|
|
|
|
let published_at = if draft {
|
|
None
|
|
} else {
|
|
existing.published_at.or(Some(now))
|
|
};
|
|
|
|
let r = sqlx::query_as::<_, RepoReleaseModel>(
|
|
"UPDATE repo_release SET tag_name=$1, name=$2, body=$3, draft=$4, \
|
|
prerelease=$5, published_at=$6, updated_at=$7 \
|
|
WHERE id=$8 AND repo=$9 \
|
|
RETURNING id, repo, tag_name, target_commit_sha, name, body, \
|
|
draft, prerelease, author, published_at, created_at, updated_at",
|
|
)
|
|
.bind(&tag_name)
|
|
.bind(&name)
|
|
.bind(&body)
|
|
.bind(draft)
|
|
.bind(prerelease)
|
|
.bind(published_at)
|
|
.bind(now)
|
|
.bind(release_id)
|
|
.bind(repo_id)
|
|
.fetch_one(self.db.writer())
|
|
.await
|
|
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
|
|
|
|
let assets = self.git_release_assets(r.id).await?;
|
|
Ok(release_to_response(r, assets))
|
|
}
|
|
|
|
pub async fn git_release_delete(
|
|
&self,
|
|
repo_id: Uuid,
|
|
release_id: Uuid,
|
|
) -> Result<(), AppError> {
|
|
let rows =
|
|
sqlx::query("DELETE FROM repo_release WHERE id = $1 AND repo = $2")
|
|
.bind(release_id)
|
|
.bind(repo_id)
|
|
.execute(self.db.writer())
|
|
.await
|
|
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
|
|
if rows.rows_affected() == 0 {
|
|
return Err(AppError::NotFound("release not found".to_string()));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn git_release_delete_by_tag(
|
|
&self,
|
|
repo_id: Uuid,
|
|
tag: &str,
|
|
) -> Result<(), AppError> {
|
|
let release_id: Option<Uuid> = sqlx::query_scalar(
|
|
"SELECT id FROM repo_release WHERE repo = $1 AND tag_name = $2",
|
|
)
|
|
.bind(repo_id)
|
|
.bind(tag)
|
|
.fetch_optional(self.db.reader())
|
|
.await
|
|
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
|
|
let release_id = release_id.ok_or_else(|| {
|
|
AppError::NotFound("release not found".to_string())
|
|
})?;
|
|
self.git_release_delete(repo_id, release_id).await
|
|
}
|
|
|
|
async fn git_release_assets(
|
|
&self,
|
|
release_id: Uuid,
|
|
) -> Result<Vec<ReleaseAssetResponse>, AppError> {
|
|
let assets = sqlx::query_as::<_, RepoReleaseAssetModel>(
|
|
"SELECT id, release_id, name, content_type, size, download_count, \
|
|
storage_path, uploader, created_at \
|
|
FROM repo_release_asset WHERE release_id = $1 ORDER BY created_at",
|
|
)
|
|
.bind(release_id)
|
|
.fetch_all(self.db.reader())
|
|
.await
|
|
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
|
|
|
|
Ok(assets
|
|
.into_iter()
|
|
.map(|a| ReleaseAssetResponse {
|
|
id: a.id,
|
|
name: a.name,
|
|
content_type: a.content_type,
|
|
size: a.size,
|
|
download_count: a.download_count,
|
|
created_at: a.created_at,
|
|
})
|
|
.collect())
|
|
}
|
|
|
|
pub async fn git_release_asset_create(
|
|
&self,
|
|
repo_id: Uuid,
|
|
release_id: Uuid,
|
|
user_id: Uuid,
|
|
name: String,
|
|
content_type: Option<String>,
|
|
size: i64,
|
|
storage_path: String,
|
|
) -> Result<ReleaseAssetResponse, AppError> {
|
|
let _ = self.git_release_get(repo_id, release_id).await?;
|
|
|
|
let id = Uuid::now_v7();
|
|
let now = Utc::now();
|
|
let a = sqlx::query_as::<_, RepoReleaseAssetModel>(
|
|
"INSERT INTO repo_release_asset (id, release_id, name, content_type, size, \
|
|
download_count, storage_path, uploader, created_at) \
|
|
VALUES ($1,$2,$3,$4,$5,0,$6,$7,$8) RETURNING *",
|
|
)
|
|
.bind(id)
|
|
.bind(release_id)
|
|
.bind(&name)
|
|
.bind(&content_type)
|
|
.bind(size)
|
|
.bind(&storage_path)
|
|
.bind(user_id)
|
|
.bind(now)
|
|
.fetch_one(self.db.writer())
|
|
.await
|
|
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
|
|
|
|
Ok(ReleaseAssetResponse {
|
|
id: a.id,
|
|
name: a.name,
|
|
content_type: a.content_type,
|
|
size: a.size,
|
|
download_count: a.download_count,
|
|
created_at: a.created_at,
|
|
})
|
|
}
|
|
|
|
pub async fn git_release_asset_delete(
|
|
&self,
|
|
repo_id: Uuid,
|
|
release_id: Uuid,
|
|
asset_id: Uuid,
|
|
) -> Result<(), AppError> {
|
|
let _ = self.git_release_get(repo_id, release_id).await?;
|
|
sqlx::query(
|
|
"DELETE FROM repo_release_asset WHERE id = $1 AND release_id = $2",
|
|
)
|
|
.bind(asset_id)
|
|
.bind(release_id)
|
|
.execute(self.db.writer())
|
|
.await
|
|
.map_err(|e| AppError::DatabaseError(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl AppService {
|
|
pub async fn git_release_list_by_name(
|
|
&self,
|
|
ctx: &Session,
|
|
_user_id: Uuid,
|
|
wk: &str,
|
|
repo: &str,
|
|
) -> Result<Vec<ReleaseResponse>, AppError> {
|
|
let repo = self.git_require_member(ctx, wk, repo).await?;
|
|
self.git_release_list(repo.id).await
|
|
}
|
|
pub async fn git_release_get_by_name(
|
|
&self,
|
|
ctx: &Session,
|
|
_user_id: Uuid,
|
|
wk: &str,
|
|
repo: &str,
|
|
id: Uuid,
|
|
) -> Result<ReleaseResponse, AppError> {
|
|
let repo = self.git_require_member(ctx, wk, repo).await?;
|
|
self.git_release_get(repo.id, id).await
|
|
}
|
|
pub async fn git_release_get_by_tag_name(
|
|
&self,
|
|
ctx: &Session,
|
|
_user_id: Uuid,
|
|
wk: &str,
|
|
repo: &str,
|
|
tag: &str,
|
|
) -> Result<ReleaseResponse, AppError> {
|
|
let repo = self.git_require_member(ctx, wk, repo).await?;
|
|
self.git_release_get_by_tag(repo.id, tag).await
|
|
}
|
|
pub async fn git_release_create_by_name(
|
|
&self,
|
|
ctx: &Session,
|
|
user_id: Uuid,
|
|
wk: &str,
|
|
repo: &str,
|
|
params: CreateRelease,
|
|
) -> Result<ReleaseResponse, AppError> {
|
|
let repo = self.git_require_member(ctx, wk, repo).await?;
|
|
self.git_release_create(ctx, repo.id, user_id, params).await
|
|
}
|
|
pub async fn git_release_update_by_name(
|
|
&self,
|
|
ctx: &Session,
|
|
_user_id: Uuid,
|
|
wk: &str,
|
|
repo: &str,
|
|
id: Uuid,
|
|
params: UpdateRelease,
|
|
) -> Result<ReleaseResponse, AppError> {
|
|
let repo = self.git_require_member(ctx, wk, repo).await?;
|
|
self.git_release_update(repo.id, id, params).await
|
|
}
|
|
pub async fn git_release_delete_by_name(
|
|
&self,
|
|
ctx: &Session,
|
|
_user_id: Uuid,
|
|
wk: &str,
|
|
repo: &str,
|
|
id: Uuid,
|
|
) -> Result<(), AppError> {
|
|
let repo = self.git_require_member(ctx, wk, repo).await?;
|
|
self.git_release_delete(repo.id, id).await
|
|
}
|
|
pub async fn git_release_delete_by_tag_name(
|
|
&self,
|
|
ctx: &Session,
|
|
_user_id: Uuid,
|
|
wk: &str,
|
|
repo: &str,
|
|
tag: &str,
|
|
) -> Result<(), AppError> {
|
|
let repo = self.git_require_member(ctx, wk, repo).await?;
|
|
self.git_release_delete_by_tag(repo.id, tag).await
|
|
}
|
|
}
|
|
|
|
impl AppService {
|
|
async fn default_branch_sha(
|
|
&self,
|
|
repo_id: Uuid,
|
|
) -> Result<String, AppError> {
|
|
sqlx::query_scalar("SELECT target_sha FROM repo_ref WHERE repo = $1 AND is_default = true")
|
|
.bind(repo_id)
|
|
.fetch_optional(self.db.reader())
|
|
.await
|
|
.map_err(|e| AppError::DatabaseError(e.to_string()))?
|
|
.ok_or_else(|| AppError::BadRequest("no default branch to target".to_string()))
|
|
}
|
|
}
|
|
|
|
fn release_to_response(
|
|
r: RepoReleaseModel,
|
|
assets: Vec<ReleaseAssetResponse>,
|
|
) -> ReleaseResponse {
|
|
ReleaseResponse {
|
|
id: r.id,
|
|
tag_name: r.tag_name,
|
|
target_commit_sha: r.target_commit_sha,
|
|
name: r.name,
|
|
body: r.body,
|
|
draft: r.draft,
|
|
prerelease: r.prerelease,
|
|
author: r.author,
|
|
assets,
|
|
published_at: r.published_at,
|
|
created_at: r.created_at,
|
|
}
|
|
}
|