refactor(ci): switch 1Panel appstore to community repo and manual trigger

This commit is contained in:
t8y2
2026-05-11 11:41:51 +08:00
parent 2d83fbb1ed
commit 70db0aa62c
10 changed files with 293 additions and 24 deletions
+13 -18
View File
@@ -1,46 +1,41 @@
name: 1Panel App Store
on:
release:
types: [published, released]
workflow_dispatch:
inputs:
version:
description: "Version to submit (e.g. 0.5.3)"
required: true
jobs:
submit:
runs-on: ubuntu-latest
if: ${{ !github.event.release.draft && !github.event.release.prerelease }}
env:
REPO_OWNER: t8y2
steps:
- uses: actions/checkout@v4
- name: Extract version from tag
id: version
run: |
VERSION="${{ github.event.release.tag_name }}"
VERSION="${VERSION#v}"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
- name: Submit to 1Panel App Store
env:
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
run: |
VERSION="${{ steps.version.outputs.version }}"
VERSION="${{ github.event.inputs.version }}"
git clone --depth 1 \
"https://x-access-token:${TAP_GITHUB_TOKEN}@github.com/${REPO_OWNER}/appstore.git" \
git clone --depth 1 -b localApps \
"https://x-access-token:${TAP_GITHUB_TOKEN}@github.com/${REPO_OWNER}/appstore-1panel.git" \
appstore
cd appstore
APP_DIR="apps/dbx"
mkdir -p "${APP_DIR}/${VERSION}"
mkdir -p "${APP_DIR}/latest"
cp "${{ github.workspace }}/1panel/data.yml" "${APP_DIR}/data.yml"
cp "${{ github.workspace }}/1panel/README.md" "${APP_DIR}/README.md"
if [ -f "${{ github.workspace }}/1panel/logo.png" ]; then
cp "${{ github.workspace }}/1panel/logo.png" "${APP_DIR}/logo.png"
fi
cp -r "${{ github.workspace }}/1panel/latest/." "${APP_DIR}/${VERSION}/"
cp -r "${{ github.workspace }}/1panel/latest/." "${APP_DIR}/latest/"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
@@ -55,10 +50,10 @@ jobs:
git commit -m "feat(dbx): release ${VERSION}"
git push origin "${BRANCH}"
gh pr create \
--repo "1Panel-dev/appstore" \
--repo "okxlin/appstore" \
--head "${REPO_OWNER}:${BRANCH}" \
--title "feat(dbx): release ${VERSION}" \
--body "$(printf 'Automated release of DBX v%s\n\n```release-note\nfeat(dbx): release %s\n```' "${VERSION}" "${VERSION}")" \
--base dev
--body "Automated release of DBX v${VERSION}" \
--base localApps
echo "::notice::1Panel App Store PR created for ${VERSION}"
fi
+12 -1
View File
@@ -1,13 +1,24 @@
additional:
name: DBX
tags:
- 工具
title: 轻量级数据库管理工具
description: 轻量级数据库管理工具,支持 25+ 种数据库
additionalProperties:
key: dbx
name: DBX
tags:
- Tool
shortDescZh: 轻量级数据库管理工具,支持 25+ 种数据库
shortDescEn: Lightweight database manager supporting 25+ databases
description:
en: Lightweight database manager supporting 25+ databases
zh: 轻量级数据库管理工具,支持 25+ 种数据库
type: tool
crossVersionUpdate: true
limit: 0
recommend: 0
website: https://github.com/t8y2/dbx
github: https://github.com/t8y2/dbx
document: https://github.com/t8y2/dbx#readme
architectures:
- amd64
+6
View File
@@ -1,7 +1,10 @@
additionalProperties:
formFields:
- default: 4224
edit: true
envKey: PANEL_APP_PORT_HTTP
labelZh: 端口
labelEn: Port
label:
zh: 端口
en: Port
@@ -10,7 +13,10 @@ additionalProperties:
type: number
- default: dbx_password
edit: true
envKey: PANEL_DB_PASSWORD
labelZh: 访问密码
labelEn: Access Password
label:
zh: 访问密码
en: Access Password
+5 -5
View File
@@ -1,11 +1,9 @@
version: "3"
services:
${CONTAINER_NAME}:
image: t8y2/dbx:${PANEL_APP_VERSION_TAG}
dbx:
image: t8y2/dbx:latest
container_name: ${CONTAINER_NAME}
ports:
- ${PANEL_APP_PORT_HTTP}:4224
- "${PANEL_APP_PORT_HTTP}:4224"
environment:
- DBX_PASSWORD=${PANEL_DB_PASSWORD}
volumes:
@@ -13,6 +11,8 @@ services:
restart: unless-stopped
networks:
- 1panel-network
labels:
createdBy: "Apps"
networks:
1panel-network:
+211
View File
@@ -677,6 +677,217 @@ pub async fn get_table_ddl_core(
}
}
fn sql_string(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}
fn pg_ident(value: &str) -> String {
format!("\"{}\"", value.replace('"', "\"\""))
}
fn mysql_ident(value: &str) -> String {
format!("`{}`", value.replace('`', "``"))
}
fn sqlite_object_type(kind: &db::ObjectSourceKind) -> &'static str {
match kind {
db::ObjectSourceKind::View => "view",
db::ObjectSourceKind::Procedure | db::ObjectSourceKind::Function => "routine",
}
}
fn sqlserver_object_type_filter(kind: &db::ObjectSourceKind) -> &'static str {
match kind {
db::ObjectSourceKind::View => "'V'",
db::ObjectSourceKind::Procedure => "'P'",
db::ObjectSourceKind::Function => "'FN','IF','TF','FS','FT'",
}
}
pub fn sqlserver_object_source_sql(schema: &str, name: &str, kind: &db::ObjectSourceKind) -> String {
format!(
"SELECT m.definition FROM sys.sql_modules m \
JOIN sys.objects o ON o.object_id = m.object_id \
JOIN sys.schemas s ON s.schema_id = o.schema_id \
WHERE s.name = {} AND o.name = {} AND o.type IN ({})",
sql_string(schema),
sql_string(name),
sqlserver_object_type_filter(kind)
)
}
pub fn postgres_object_source_sql(schema: &str, name: &str, kind: &db::ObjectSourceKind) -> String {
match kind {
db::ObjectSourceKind::View => {
format!("SELECT pg_get_viewdef('{}.{}'::regclass, true)", pg_ident(schema), pg_ident(name))
}
db::ObjectSourceKind::Procedure | db::ObjectSourceKind::Function => {
let prokind = if matches!(kind, db::ObjectSourceKind::Procedure) { "p" } else { "f" };
format!(
"SELECT pg_get_functiondef(p.oid) \
FROM pg_proc p \
JOIN pg_namespace n ON n.oid = p.pronamespace \
WHERE n.nspname = {} AND p.proname = {} AND p.prokind = '{}' \
ORDER BY p.oid LIMIT 1",
sql_string(schema),
sql_string(name),
prokind
)
}
}
}
pub fn oracle_object_source_sql(schema: &str, name: &str, kind: &db::ObjectSourceKind) -> String {
let object_type = match kind {
db::ObjectSourceKind::View => "VIEW",
db::ObjectSourceKind::Procedure => "PROCEDURE",
db::ObjectSourceKind::Function => "FUNCTION",
};
format!(
"SELECT DBMS_METADATA.GET_DDL({}, {}, {}) FROM DUAL",
sql_string(object_type),
sql_string(name),
sql_string(schema)
)
}
pub fn sqlite_object_source_sql(name: &str, kind: &db::ObjectSourceKind) -> String {
format!(
"SELECT sql FROM sqlite_master WHERE type = {} AND name = {}",
sql_string(sqlite_object_type(kind)),
sql_string(name)
)
}
pub fn mysql_object_source_sql(name: &str, kind: &db::ObjectSourceKind) -> String {
match kind {
db::ObjectSourceKind::View => format!("SHOW CREATE VIEW {}", mysql_ident(name)),
db::ObjectSourceKind::Procedure => format!("SHOW CREATE PROCEDURE {}", mysql_ident(name)),
db::ObjectSourceKind::Function => format!("SHOW CREATE FUNCTION {}", mysql_ident(name)),
}
}
fn first_string_cell(result: db::QueryResult) -> Result<String, String> {
result
.rows
.first()
.and_then(|row| row.iter().find_map(|value| value.as_str().map(str::to_string)))
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| "Object source not found".to_string())
}
async fn mysql_object_source(
pool: &sqlx::mysql::MySqlPool,
name: &str,
kind: &db::ObjectSourceKind,
) -> Result<String, String> {
use sqlx::Row;
let sql = mysql_object_source_sql(name, kind);
let row: sqlx::mysql::MySqlRow = sqlx::raw_sql(&sql).fetch_one(pool).await.map_err(|e| e.to_string())?;
let index = if matches!(kind, db::ObjectSourceKind::View) { 1 } else { 2 };
row.try_get::<String, _>(index)
.or_else(|_| row.try_get::<Vec<u8>, _>(index).map(|b| String::from_utf8_lossy(&b).to_string()))
.map_err(|e| e.to_string())
}
pub async fn get_object_source_core(
state: &AppState,
connection_id: &str,
database: &str,
schema: &str,
name: &str,
object_type: db::ObjectSourceKind,
) -> Result<db::ObjectSource, String> {
let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
let source = {
let connections = state.connections.read().await;
if let Some(client) = extract_sqlserver(&connections, &pool_key) {
drop(connections);
let mut client = client.lock().await;
first_string_cell(
db::sqlserver::execute_query(&mut client, &sqlserver_object_source_sql(schema, name, &object_type))
.await?,
)?
} else if let Some(pool) = extract_oracle(&connections, &pool_key) {
drop(connections);
let client = pool.client();
let client = client.lock().await;
first_string_cell(
db::oracle_driver::execute_query(&*client, &oracle_object_source_sql(schema, name, &object_type))
.await?,
)?
} else if let Some(client) = extract_gaussdb(&connections, &pool_key) {
drop(connections);
let mut client = client.lock().await;
first_string_cell(
db::gaussdb_driver::execute_query(&mut client, &postgres_object_source_sql(schema, name, &object_type))
.await?,
)?
} else {
match connections.get(&pool_key).ok_or("Pool not found")? {
PoolKind::Mysql(pool, _) => mysql_object_source(pool, name, &object_type).await?,
PoolKind::Postgres(pool) => first_string_cell(
db::postgres::execute_query(pool, &postgres_object_source_sql(schema, name, &object_type)).await?,
)?,
PoolKind::Sqlite(pool) => first_string_cell(
db::sqlite::execute_query(pool, &sqlite_object_source_sql(name, &object_type)).await?,
)?,
PoolKind::ClickHouse(client) if matches!(object_type, db::ObjectSourceKind::View) => {
let result = db::clickhouse_driver::execute_query(
client,
database,
&format!("SHOW CREATE TABLE {}", mysql_ident(name)),
)
.await?;
first_string_cell(result)?
}
_ => return Err("Object source is not supported for this database type".to_string()),
}
}
};
Ok(db::ObjectSource {
name: name.to_string(),
object_type,
schema: if schema.is_empty() { None } else { Some(schema.to_string()) },
source,
})
}
#[cfg(test)]
mod object_source_tests {
use super::*;
use crate::types::ObjectSourceKind;
#[test]
fn builds_sqlserver_object_source_sql_for_schema_scoped_routines() {
assert_eq!(
sqlserver_object_source_sql("dbo", "refresh_cache", &ObjectSourceKind::Procedure),
"SELECT m.definition FROM sys.sql_modules m JOIN sys.objects o ON o.object_id = m.object_id JOIN sys.schemas s ON s.schema_id = o.schema_id WHERE s.name = 'dbo' AND o.name = 'refresh_cache' AND o.type IN ('P')"
);
}
#[test]
fn builds_postgres_object_source_sql_for_views_and_functions() {
assert_eq!(
postgres_object_source_sql("public", "active_users", &ObjectSourceKind::View),
"SELECT pg_get_viewdef('\"public\".\"active_users\"'::regclass, true)"
);
assert_eq!(
postgres_object_source_sql("public", "recalc_score", &ObjectSourceKind::Function),
"SELECT pg_get_functiondef(p.oid) FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname = 'public' AND p.proname = 'recalc_score' AND p.prokind = 'f' ORDER BY p.oid LIMIT 1"
);
}
#[test]
fn builds_oracle_object_source_sql_using_metadata_api() {
assert_eq!(
oracle_object_source_sql("HR", "ACTIVE_USERS", &ObjectSourceKind::View),
"SELECT DBMS_METADATA.GET_DDL('VIEW', 'ACTIVE_USERS', 'HR') FROM DUAL"
);
}
}
pub async fn mysql_ddl(pool: &sqlx::mysql::MySqlPool, table: &str) -> Result<String, String> {
use sqlx::Row;
let sql = format!("SHOW CREATE TABLE `{}`", table.replace('`', "``"));
+16
View File
@@ -20,6 +20,22 @@ pub struct ObjectInfo {
pub comment: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ObjectSourceKind {
View,
Procedure,
Function,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObjectSource {
pub name: String,
pub object_type: ObjectSourceKind,
pub schema: Option<String>,
pub source: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColumnInfo {
pub name: String,
+12
View File
@@ -41,6 +41,18 @@ pub async fn list_objects(
dbx_core::schema::list_objects_core(&state, &connection_id, &database, &schema).await
}
#[tauri::command]
pub async fn get_object_source(
state: State<'_, Arc<AppState>>,
connection_id: String,
database: String,
schema: String,
name: String,
object_type: db::ObjectSourceKind,
) -> Result<db::ObjectSource, String> {
dbx_core::schema::get_object_source_core(&state, &connection_id, &database, &schema, &name, object_type).await
}
#[tauri::command]
pub async fn get_columns(
state: State<'_, Arc<AppState>>,
+1
View File
@@ -83,6 +83,7 @@ pub fn run() {
commands::schema::list_databases,
commands::schema::list_tables,
commands::schema::list_objects,
commands::schema::get_object_source,
commands::schema::list_schemas,
commands::schema::get_columns,
commands::schema::list_indexes,
+1
View File
@@ -87,6 +87,7 @@ async fn main() {
.route("/schema/schemas", get(routes::schema::list_schemas))
.route("/schema/tables", get(routes::schema::list_tables))
.route("/schema/objects", get(routes::schema::list_objects))
.route("/schema/object-source", get(routes::schema::get_object_source))
.route("/schema/columns", get(routes::schema::list_columns))
.route("/schema/indexes", get(routes::schema::list_indexes))
.route("/schema/foreign-keys", get(routes::schema::list_foreign_keys))
+16
View File
@@ -13,6 +13,7 @@ pub struct SchemaQuery {
pub database: Option<String>,
pub schema: Option<String>,
pub table: Option<String>,
pub object_type: Option<dbx_core::db::ObjectSourceKind>,
}
pub async fn list_databases(
@@ -54,6 +55,21 @@ pub async fn list_objects(
Ok(Json(serde_json::to_value(result).map_err(|e| AppError(e.to_string()))?))
}
pub async fn get_object_source(
State(state): State<Arc<WebState>>,
Query(q): Query<SchemaQuery>,
) -> Result<Json<dbx_core::db::ObjectSource>, AppError> {
let database = q.database.as_deref().unwrap_or("");
let schema = q.schema.as_deref().unwrap_or("");
let name = q.table.as_deref().unwrap_or("");
let object_type = q.object_type.ok_or_else(|| AppError("Missing object_type".to_string()))?;
let result =
dbx_core::schema::get_object_source_core(&state.app, &q.connection_id, database, schema, name, object_type)
.await
.map_err(AppError)?;
Ok(Json(result))
}
pub async fn list_columns(
State(state): State<Arc<WebState>>,
Query(q): Query<SchemaQuery>,