mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-10 16:00:31 +00:00
Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3c7fe320d | ||
|
|
14728f889e | ||
|
|
ff199245b0 | ||
|
|
667807e249 | ||
|
|
63e09c8c47 | ||
|
|
17a8835d59 | ||
|
|
b306bfa77a | ||
|
|
b1eff2a893 | ||
|
|
5ac7c80881 | ||
|
|
d402f67fc3 | ||
|
|
c13793c445 | ||
|
|
daa3f6b206 | ||
|
|
a2d85c911d | ||
|
|
1b76c91e1b | ||
|
|
d663a4397a | ||
|
|
d1558c5eae | ||
|
|
0da4f77a2e | ||
|
|
e7ef35740c | ||
|
|
b0d561e33b | ||
|
|
75b362fa9b | ||
|
|
466f373ef6 | ||
|
|
4489641b54 | ||
|
|
1971d9dec2 | ||
|
|
c38d741770 | ||
|
|
122a474032 | ||
|
|
54854e99e7 | ||
|
|
e01fb7c905 | ||
|
|
83a3675a0c | ||
|
|
9dfa451fae | ||
|
|
9204a287fd | ||
|
|
f027507f9a | ||
|
|
01aee89f3b | ||
|
|
17285d4397 | ||
|
|
91ae846418 | ||
|
|
1eb429631d | ||
|
|
6584e3b868 | ||
|
|
14e2294db9 | ||
|
|
26b69bc208 | ||
|
|
2111d4c16b | ||
|
|
b682835b4e | ||
|
|
c34ac2f31e | ||
|
|
c28047374a | ||
|
|
60d16609f7 | ||
|
|
02ca9347dc | ||
|
|
d0187994ee | ||
|
|
be672d604f | ||
|
|
16e3d496eb | ||
|
|
9159ab842d | ||
|
|
0d8835d494 | ||
|
|
9f9e3aacfc | ||
|
|
7bd11fe73e | ||
|
|
e11dc5fadd | ||
|
|
f35b0deb29 | ||
|
|
33b3eecb95 | ||
|
|
6ac7a5f167 | ||
|
|
a5fbbfc460 | ||
|
|
563df3da71 | ||
|
|
13e6bc4fac | ||
|
|
357d77856a | ||
|
|
4279339189 | ||
|
|
380033dfe0 | ||
|
|
97d383ed12 | ||
|
|
52e6a63ebc | ||
|
|
7506e709e2 | ||
|
|
cf37de66fc | ||
|
|
e2754b447d |
@@ -1,9 +1,6 @@
|
|||||||
<p align="center"><a href="https://1panel.pro"><img src="https://resource.1panel.pro/img/1panel-logo.png" alt="1Panel" width="300" /></a></p>
|
<p align="center"><a href="https://1panel.pro"><img src="https://resource.1panel.pro/img/1panel-logo.png" alt="1Panel" width="300" /></a></p>
|
||||||
|
|
||||||
<h3 align="center">The open-source VPS control panel with native AI agent support</h3>
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Trusted by <strong>2,000,000+</strong> self-hosters worldwide
|
Loved by a global community of <strong>2.5M+</strong> self-hosters.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -12,7 +9,6 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://shields.io/github/license/1Panel-dev/1Panel?color=%231890FF" alt="License: GPL v3"></a>
|
<a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://shields.io/github/license/1Panel-dev/1Panel?color=%231890FF" alt="License: GPL v3"></a>
|
||||||
<a href="https://app.codacy.com/gh/1Panel-dev/1Panel"><img src="https://app.codacy.com/project/badge/Grade/da67574fd82b473992781d1386b937ef" alt="Codacy"></a>
|
|
||||||
<a href="https://discord.gg/bUpUqWqdRr"><img src="https://img.shields.io/discord/1318846410149335080?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb" alt="Discord"></a>
|
<a href="https://discord.gg/bUpUqWqdRr"><img src="https://img.shields.io/discord/1318846410149335080?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb" alt="Discord"></a>
|
||||||
<a href="https://github.com/1Panel-dev/1Panel/releases"><img src="https://img.shields.io/github/v/release/1Panel-dev/1Panel" alt="GitHub release"></a>
|
<a href="https://github.com/1Panel-dev/1Panel/releases"><img src="https://img.shields.io/github/v/release/1Panel-dev/1Panel" alt="GitHub release"></a>
|
||||||
<a href="https://github.com/1Panel-dev/1Panel"><img src="https://img.shields.io/github/stars/1Panel-dev/1Panel?color=%231890FF&style=flat-square" alt="Stars"></a>
|
<a href="https://github.com/1Panel-dev/1Panel"><img src="https://img.shields.io/github/stars/1Panel-dev/1Panel?color=%231890FF&style=flat-square" alt="Stars"></a>
|
||||||
@@ -41,34 +37,28 @@
|
|||||||
|
|
||||||
## What is 1Panel?
|
## What is 1Panel?
|
||||||
|
|
||||||
1Panel is a modern, open-source VPS control panel — and the only one with **native AI agent support**. Run Ollama models, deploy OpenClaw agents, and manage your entire server stack from one clean web interface. No CLI memorization required.
|
1Panel is a modern, open-source Linux server management panel and a lightweight AI management platform. Through an intuitive web interface, it provides users with comprehensive, one-stop server management capabilities:
|
||||||
|
- **AI Management**: Offers a unified management platform from bare metal to agents (Metal-to-Agent). It integrates an AI gateway, and Skills Hub, while supporting centralized management of agents and models.
|
||||||
👉 Watch the [2-minute introduction](https://www.youtube.com/watch?v=Jl_wqp-XA08)
|
- **Efficient Visual Operations**: Easily manage Linux servers through a web-based GUI, streamlining tasks such as host monitoring, file management, database management, and container management.
|
||||||
|
- **Rapid Website Deployment**: Deeply integrates with popular website builders like WordPress and Halo. It enables one-click domain binding and SSL certificate configuration, significantly lowering the barrier to website creation.
|
||||||
|
- **Curated App Store**: Features a built-in store of high-quality open-source applications, providing one-click installation and upgrade services to effortlessly extend server capabilities.
|
||||||
|
- **Enterprise-Grade Security**: Deploys applications based on container technology to effectively minimize vulnerability exposure. It also provides security features such as WAF and log auditing to ensure comprehensive server protection.
|
||||||
|
- **One-Click Data Backup**: Supports one-click backup and restoration, and integrates with various cloud storage solutions to ensure data security and prevent loss.
|
||||||
|
|
||||||
## Why 1Panel?
|
## Why 1Panel?
|
||||||
|
|
||||||
| | 1Panel | cPanel / Plesk | aaPanel | Webmin |
|
| | 1Panel | cPanel / Plesk | aaPanel | Webmin |
|
||||||
|--|--------|----------------|---------|--------|
|
|--|--------|----------------|---------|--------|
|
||||||
| Free & open source | ✅ | ❌ | Partial | ✅ |
|
| Free & open source | ✅ | ❌ | Partial | ✅ |
|
||||||
| Native AI agent runtime | ✅ | ❌ | ❌ | ❌ |
|
| AI management | ✅ | ❌ | ❌ | ❌ |
|
||||||
| One-click app marketplace | ✅ 165+ apps | ❌ | ✅ | ❌ |
|
| One-click app marketplace | ✅ 165+ apps | ❌ | ✅ | ❌ |
|
||||||
| Modern UI (post-2020) | ✅ | ❌ | Partial | ❌ |
|
| Modern UI (post-2020) | ✅ | ❌ | Partial | ❌ |
|
||||||
| Docker / container management | ✅ | ❌ | ❌ | ❌ |
|
| Docker / container management | ✅ | ❌ | ❌ | ❌ |
|
||||||
| Active development | ✅ | ✅ | ✅ | Slow |
|
| Active development | ✅ | ✅ | ✅ | Slow |
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
- **AI Agent Runtime**: Deploy Ollama LLMs, spin up OpenClaw personal agents, and monitor GPU utilization — all from the dashboard. No separate AI stack to manage.
|
|
||||||
- **One-Click Website Deployment**: Launch production-ready websites with automatic domain binding, SSL provisioning, and Nginx config — zero manual setup.
|
|
||||||
- **App Marketplace**: 165+ trusted open-source apps (Nextcloud, Bitwarden, Umami, NocoBase, and more) installed and updated with a single click.
|
|
||||||
- **Docker & Container Management**: Create, start, stop, and inspect containers, images, networks, and volumes through a visual UI — no CLI juggling.
|
|
||||||
- **Security Out of the Box**: Firewall rules, fail2ban, container isolation, WAF, and audit logs — configured and running from day one.
|
|
||||||
- **Backup & Restore**: Schedule automated backups to AWS S3, Cloudflare R2, or local storage. Restore any snapshot in one click.
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
> **Requirements:** Linux VPS (Debian / Ubuntu / CentOS / Rocky), 1 GB RAM, internet access.
|
Prepare your Linux server and run the following script:
|
||||||
> Takes ~60 seconds.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash -c "$(curl -sSL https://resource.1panel.pro/v2/quick_start.sh)"
|
bash -c "$(curl -sSL https://resource.1panel.pro/v2/quick_start.sh)"
|
||||||
@@ -83,24 +73,20 @@ Run `1pctl user-info` via SSH if you need to retrieve your access credentials.
|
|||||||
|
|
||||||
## Pro Edition
|
## Pro Edition
|
||||||
|
|
||||||
1Panel OSS is free forever. Pro adds features built for teams and production workloads:
|
1Panel OSS is free forever. 1Panel Pro and Ent adds features built for teams and production workloads:
|
||||||
|
|
||||||
| Feature | OSS | Pro |
|
| Feature | OSS | Pro | Ent |
|
||||||
|---------|:---:|:---:|
|
|---------|:---:|:---:|:---:|
|
||||||
| One-click app installs | ✅ | ✅ |
|
| One-click app installs | ✅ | ✅ | ✅ |
|
||||||
| AI agents (OpenClaw) | 1 agent | Unlimited |
|
| AI agents (OpenClaw) | 5 agent | Unlimited | ✅ |
|
||||||
| WAF & advanced security | Basic | ✅ |
|
| WAF & advanced security | Basic | ✅ | ✅ |
|
||||||
| Website tamper protection | ❌ | ✅ |
|
| Website tamper protection | ❌ | ✅ | ✅ |
|
||||||
| Website uptime monitoring | ❌ | ✅ |
|
| Website uptime monitoring | ❌ | ✅ | ✅ |
|
||||||
| Multi-node management | ❌ | ✅ |
|
| Multi-node management | ❌ | ✅ | ✅ |
|
||||||
| Custom logo & theme | ❌ | ✅ |
|
| Custom logo & theme | ❌ | ✅ | ✅ |
|
||||||
| Priority support | ❌ | ✅ |
|
| KVM Web UI | ❌ | ❌ | ✅ |
|
||||||
|
| AI Gateway | ❌ | ❌ | ✅ |
|
||||||
**From $80/year.** [Compare plans & start 30-day free trial →](https://1panel.pro/pricing)
|
| Priority support | ❌ | ❌ | ✅ |
|
||||||
|
|
||||||
## Star History
|
|
||||||
|
|
||||||
[](https://star-history.com/#1Panel-dev/1Panel&Date)
|
|
||||||
|
|
||||||
## Community & Support
|
## Community & Support
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ func (b *BaseApi) PageAgents(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
total, list, err := agentService.Page(req)
|
total, list, err := agentService.Page(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
@@ -447,7 +447,7 @@ func (b *BaseApi) PageAgentAccounts(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
total, list, err := agentService.PageAccounts(req)
|
total, list, err := agentService.PageAccounts(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
@@ -1378,6 +1378,88 @@ func (b *BaseApi) UninstallAgentSkill(c *gin.Context) {
|
|||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags AI
|
||||||
|
// @Summary List OpenClaw plugins
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.AgentPluginsReq true "request"
|
||||||
|
// @Success 200 {array} dto.AgentPluginItem
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/agents/plugins/list [post]
|
||||||
|
func (b *BaseApi) ListAgentPlugins(c *gin.Context) {
|
||||||
|
var req dto.AgentPluginsReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := agentService.ListPlugins(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags AI
|
||||||
|
// @Summary Search OpenClaw plugins
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.AgentPluginSearchReq true "request"
|
||||||
|
// @Success 200 {array} dto.AgentPluginSearchItem
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/agents/plugins/search [post]
|
||||||
|
func (b *BaseApi) SearchAgentPlugins(c *gin.Context) {
|
||||||
|
var req dto.AgentPluginSearchReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := agentService.SearchPlugins(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags AI
|
||||||
|
// @Summary Install an OpenClaw marketplace plugin
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.AgentPluginMarketInstallReq true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/agents/plugins/install [post]
|
||||||
|
func (b *BaseApi) InstallAgentMarketPlugin(c *gin.Context) {
|
||||||
|
var req dto.AgentPluginMarketInstallReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := agentService.InstallMarketPlugin(req); err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags AI
|
||||||
|
// @Summary Operate an OpenClaw plugin
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.AgentPluginOperateReq true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/agents/plugins/operate [post]
|
||||||
|
func (b *BaseApi) OperateAgentPlugin(c *gin.Context) {
|
||||||
|
var req dto.AgentPluginOperateReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := agentService.OperatePlugin(req); err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Login Agent Weixin channel
|
// @Summary Login Agent Weixin channel
|
||||||
// @Accept json
|
// @Accept json
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ func (b *BaseApi) PageAlertConfig(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
total, configs, err := alertService.PageAlertConfig(req)
|
total, configs, err := alertService.PageAlertConfig(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ func (b *BaseApi) GetAppDetail(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
version := c.Param("version")
|
version := c.Param("version")
|
||||||
appType := c.Param("type")
|
appType := c.Param("type")
|
||||||
appDetailDTO, err := appService.GetAppDetail(appID, version, appType)
|
appDetailDTO, err := appService.GetAppDetail(appID, version, appType, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /apps/ignored/detail [get]
|
// @Router /apps/ignored/detail [get]
|
||||||
func (b *BaseApi) ListAppIgnored(c *gin.Context) {
|
func (b *BaseApi) ListAppIgnored(c *gin.Context) {
|
||||||
res, err := appIgnoreUpgradeService.List()
|
res, err := appIgnoreUpgradeService.List(helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ func (b *BaseApi) SearchAppInstalled(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
req.ReadOnly = helper.IsDemoRequest(c)
|
||||||
if req.All {
|
if req.All {
|
||||||
list, err := appInstallService.SearchForWebsite(req)
|
list, err := appInstallService.SearchForWebsite(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -73,6 +74,7 @@ func (b *BaseApi) CheckAppInstalled(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
req.ReadOnly = helper.IsDemoRequest(c)
|
||||||
checkData, err := appInstallService.CheckExist(req)
|
checkData, err := appInstallService.CheckExist(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
@@ -115,7 +117,7 @@ func (b *BaseApi) LoadConnInfo(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
conn, err := appInstallService.LoadConnInfo(req)
|
conn, err := appInstallService.LoadConnInfo(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -137,7 +139,7 @@ func (b *BaseApi) DeleteCheck(c *gin.Context) {
|
|||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
checkData, err := appInstallService.DeleteCheck(appInstallId)
|
checkData, err := appInstallService.DeleteCheck(appInstallId, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -277,7 +279,7 @@ func (b *BaseApi) GetParams(c *gin.Context) {
|
|||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
content, err := appInstallService.GetParams(appInstallId)
|
content, err := appInstallService.GetParams(appInstallId, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -341,7 +343,7 @@ func (b *BaseApi) GetAppInstallInfo(c *gin.Context) {
|
|||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
info, err := appInstallService.GetAppInstallInfo(appInstallId)
|
info, err := appInstallService.GetAppInstallInfo(appInstallId, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ func (b *BaseApi) SearchBackup(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
total, list, err := backupService.SearchWithPage(req)
|
total, list, err := backupService.SearchWithPage(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ func (b *BaseApi) SearchClam(c *gin.Context) {
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /toolbox/clam/base [post]
|
// @Router /toolbox/clam/base [post]
|
||||||
func (b *BaseApi) LoadClamBaseInfo(c *gin.Context) {
|
func (b *BaseApi) LoadClamBaseInfo(c *gin.Context) {
|
||||||
info, err := clamService.LoadBaseInfo()
|
info, err := clamService.LoadBaseInfo(helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func (b *BaseApi) SearchComposeTemplate(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
total, list, err := composeTemplateService.SearchWithPage(req)
|
total, list, err := composeTemplateService.SearchWithPage(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -85,7 +85,7 @@ func (b *BaseApi) SearchComposeTemplate(c *gin.Context) {
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /containers/template [get]
|
// @Router /containers/template [get]
|
||||||
func (b *BaseApi) ListComposeTemplate(c *gin.Context) {
|
func (b *BaseApi) ListComposeTemplate(c *gin.Context) {
|
||||||
list, err := composeTemplateService.List()
|
list, err := composeTemplateService.List(helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ func (b *BaseApi) SearchCompose(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
total, list, err := containerService.PageCompose(req)
|
total, list, err := containerService.PageCompose(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -387,7 +387,7 @@ func (b *BaseApi) ContainerInfo(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := containerService.ContainerInfo(req)
|
data, err := containerService.ContainerInfo(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -881,6 +881,26 @@ func (b *BaseApi) ComposeUpdate(c *gin.Context) {
|
|||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags Container Compose
|
||||||
|
// @Summary Pin compose
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.ComposePin true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /containers/compose/pin [post]
|
||||||
|
func (b *BaseApi) ComposePin(c *gin.Context) {
|
||||||
|
var req dto.ComposePin
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := containerService.ComposePin(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags Container Compose
|
// @Tags Container Compose
|
||||||
// @Summary Load compose environment variables
|
// @Summary Load compose environment variables
|
||||||
// @Accept json
|
// @Accept json
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func (b *BaseApi) SearchCronjob(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
total, list, err := cronjobService.SearchWithPage(req)
|
total, list, err := cronjobService.SearchWithPage(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ func (b *BaseApi) SearchDatabase(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
total, list, err := databaseService.SearchWithPage(req)
|
total, list, err := databaseService.SearchWithPage(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -147,7 +147,7 @@ func (b *BaseApi) GetDatabase(c *gin.Context) {
|
|||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data, err := databaseService.Get(name)
|
data, err := databaseService.Get(name, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func (b *BaseApi) SearchMongodb(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
total, list, err := mongodbService.SearchWithPage(req)
|
total, list, err := mongodbService.SearchWithPage(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func (b *BaseApi) ListMysqlUsers(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data, err := mysqlService.ListUsers(req)
|
data, err := mysqlService.ListUsers(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ func (b *BaseApi) SearchPostgresql(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
total, list, err := postgresqlService.SearchWithPage(req)
|
total, list, err := postgresqlService.SearchWithPage(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ var (
|
|||||||
fileShareService = service.NewIFileShareService()
|
fileShareService = service.NewIFileShareService()
|
||||||
sshService = service.NewISSHService()
|
sshService = service.NewISSHService()
|
||||||
firewallService = service.NewIFirewallService()
|
firewallService = service.NewIFirewallService()
|
||||||
|
forwardingService = service.NewIForwardingService()
|
||||||
iptablesService = service.NewIIptablesService()
|
iptablesService = service.NewIIptablesService()
|
||||||
monitorService = service.NewIMonitorService()
|
monitorService = service.NewIMonitorService()
|
||||||
systemService = service.NewISystemService()
|
systemService = service.NewISystemService()
|
||||||
@@ -60,6 +61,7 @@ var (
|
|||||||
websiteDnsAccountService = service.NewIWebsiteDnsAccountService()
|
websiteDnsAccountService = service.NewIWebsiteDnsAccountService()
|
||||||
websiteSSLService = service.NewIWebsiteSSLService()
|
websiteSSLService = service.NewIWebsiteSSLService()
|
||||||
websiteAcmeAccountService = service.NewIWebsiteAcmeAccountService()
|
websiteAcmeAccountService = service.NewIWebsiteAcmeAccountService()
|
||||||
|
websiteTemplateService = service.NewIWebsiteTemplateService()
|
||||||
|
|
||||||
nginxService = service.NewINginxService()
|
nginxService = service.NewINginxService()
|
||||||
|
|
||||||
|
|||||||
+451
-29
@@ -35,6 +35,81 @@ var cancelledChunkUploads = struct {
|
|||||||
ids map[string]struct{}
|
ids map[string]struct{}
|
||||||
}{ids: make(map[string]struct{})}
|
}{ids: make(map[string]struct{})}
|
||||||
|
|
||||||
|
type chunkUploadLock struct {
|
||||||
|
mutex sync.Mutex
|
||||||
|
refs int
|
||||||
|
}
|
||||||
|
|
||||||
|
var chunkUploadLocks = struct {
|
||||||
|
sync.Mutex
|
||||||
|
items map[string]*chunkUploadLock
|
||||||
|
}{items: make(map[string]*chunkUploadLock)}
|
||||||
|
|
||||||
|
type completedChunkUpload struct {
|
||||||
|
dstDir string
|
||||||
|
filename string
|
||||||
|
fileSize int64
|
||||||
|
}
|
||||||
|
|
||||||
|
var completedChunkUploads = struct {
|
||||||
|
sync.RWMutex
|
||||||
|
items map[string]completedChunkUpload
|
||||||
|
}{items: make(map[string]completedChunkUpload)}
|
||||||
|
|
||||||
|
var activeChunkUploadTTL = 24 * time.Hour
|
||||||
|
|
||||||
|
var (
|
||||||
|
errChunkUploadCancelled = errors.New("upload cancelled")
|
||||||
|
errInvalidChunkUpload = errors.New("invalid chunk upload")
|
||||||
|
)
|
||||||
|
|
||||||
|
type activeChunkUpload struct {
|
||||||
|
upload completedChunkUpload
|
||||||
|
expiresAt time.Time
|
||||||
|
timer *time.Timer
|
||||||
|
}
|
||||||
|
|
||||||
|
var activeChunkUploads = struct {
|
||||||
|
sync.RWMutex
|
||||||
|
items map[string]activeChunkUpload
|
||||||
|
}{items: make(map[string]activeChunkUpload)}
|
||||||
|
|
||||||
|
type resumableUploadChunk struct {
|
||||||
|
UploadID string
|
||||||
|
Filename string
|
||||||
|
DstDir string
|
||||||
|
ChunkIndex int
|
||||||
|
ChunkCount int
|
||||||
|
Offset int64
|
||||||
|
FileSize int64
|
||||||
|
Overwrite bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidChunkUploadError(message string) error {
|
||||||
|
return fmt.Errorf("%w: %s", errInvalidChunkUpload, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRetryableChunkUploadError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if errors.Is(err, errChunkUploadCancelled) ||
|
||||||
|
errors.Is(err, errInvalidChunkUpload) ||
|
||||||
|
errors.Is(err, os.ErrExist) ||
|
||||||
|
errors.Is(err, os.ErrPermission) ||
|
||||||
|
errors.Is(err, os.ErrInvalid) ||
|
||||||
|
errors.Is(err, syscall.ENOSPC) ||
|
||||||
|
errors.Is(err, syscall.EDQUOT) ||
|
||||||
|
errors.Is(err, syscall.EROFS) ||
|
||||||
|
errors.Is(err, syscall.EFBIG) ||
|
||||||
|
errors.Is(err, syscall.ENAMETOOLONG) ||
|
||||||
|
errors.Is(err, syscall.ENOTDIR) ||
|
||||||
|
errors.Is(err, syscall.EISDIR) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags File
|
// @Tags File
|
||||||
// @Summary List files
|
// @Summary List files
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -73,7 +148,7 @@ func (b *BaseApi) FileAISearch(c *gin.Context) {
|
|||||||
if strings.TrimSpace(req.ResponseLanguage) == "" {
|
if strings.TrimSpace(req.ResponseLanguage) == "" {
|
||||||
req.ResponseLanguage = strings.TrimSpace(c.GetHeader("Accept-Language"))
|
req.ResponseLanguage = strings.TrimSpace(c.GetHeader("Accept-Language"))
|
||||||
}
|
}
|
||||||
res, err := fileService.AISearch(req)
|
res, err := fileService.AISearch(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -474,11 +549,7 @@ func (b *BaseApi) UploadFiles(c *gin.Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dstInfo, statErr := os.Stat(dstFilename)
|
dstInfo, statErr := os.Stat(dstFilename)
|
||||||
if overwrite {
|
err = finalizeUploadedFile(tmpFilename, dstFilename, overwrite)
|
||||||
_ = os.Remove(dstFilename)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.Rename(tmpFilename, dstFilename)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = os.Remove(tmpFilename)
|
_ = os.Remove(tmpFilename)
|
||||||
e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err)
|
e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err)
|
||||||
@@ -638,6 +709,26 @@ func (b *BaseApi) MoveFile(c *gin.Context) {
|
|||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags File
|
||||||
|
// @Summary Stop file move task
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.FileMoveStopReq true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /files/move/stop [post]
|
||||||
|
func (b *BaseApi) StopMoveFile(c *gin.Context) {
|
||||||
|
var req request.FileMoveStopReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := fileService.StopMvFile(req.TaskID); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags File
|
// @Tags File
|
||||||
// @Summary Download file
|
// @Summary Download file
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -792,6 +883,289 @@ func (b *BaseApi) DepthDirSize(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, res)
|
helper.SuccessWithData(c, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func lockChunkUpload(uploadID string) func() {
|
||||||
|
chunkUploadLocks.Lock()
|
||||||
|
lock, ok := chunkUploadLocks.items[uploadID]
|
||||||
|
if !ok {
|
||||||
|
lock = &chunkUploadLock{}
|
||||||
|
chunkUploadLocks.items[uploadID] = lock
|
||||||
|
}
|
||||||
|
lock.refs++
|
||||||
|
chunkUploadLocks.Unlock()
|
||||||
|
|
||||||
|
lock.mutex.Lock()
|
||||||
|
return func() {
|
||||||
|
lock.mutex.Unlock()
|
||||||
|
chunkUploadLocks.Lock()
|
||||||
|
lock.refs--
|
||||||
|
if lock.refs == 0 {
|
||||||
|
delete(chunkUploadLocks.items, uploadID)
|
||||||
|
}
|
||||||
|
chunkUploadLocks.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resumableUploadPartPath(dstDir, uploadID string) string {
|
||||||
|
return filepath.Join(dstDir, fmt.Sprintf(".1panel-upload-%s.part", uploadID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func finalizeUploadedFile(tmpFile, dstFile string, overwrite bool) error {
|
||||||
|
if overwrite {
|
||||||
|
return os.Rename(tmpFile, dstFile)
|
||||||
|
}
|
||||||
|
if err := os.Link(tmpFile, dstFile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.Remove(tmpFile); err != nil {
|
||||||
|
if rollbackErr := os.Remove(dstFile); rollbackErr != nil {
|
||||||
|
return fmt.Errorf("remove upload temporary file failed: %v, rollback destination failed: %w", err, rollbackErr)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerActiveChunkUpload(uploadID string, upload completedChunkUpload) error {
|
||||||
|
activeChunkUploads.Lock()
|
||||||
|
defer activeChunkUploads.Unlock()
|
||||||
|
if active, ok := activeChunkUploads.items[uploadID]; ok {
|
||||||
|
if active.upload != upload {
|
||||||
|
return invalidChunkUploadError("upload ID is already used by another file")
|
||||||
|
}
|
||||||
|
active.timer.Stop()
|
||||||
|
}
|
||||||
|
expiresAt := time.Now().Add(activeChunkUploadTTL)
|
||||||
|
timer := time.AfterFunc(activeChunkUploadTTL, func() {
|
||||||
|
expireActiveChunkUpload(uploadID, expiresAt)
|
||||||
|
})
|
||||||
|
activeChunkUploads.items[uploadID] = activeChunkUpload{
|
||||||
|
upload: upload,
|
||||||
|
expiresAt: expiresAt,
|
||||||
|
timer: timer,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadActiveChunkUpload(uploadID string) (completedChunkUpload, bool) {
|
||||||
|
activeChunkUploads.RLock()
|
||||||
|
active, ok := activeChunkUploads.items[uploadID]
|
||||||
|
activeChunkUploads.RUnlock()
|
||||||
|
return active.upload, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteActiveChunkUpload(uploadID string) {
|
||||||
|
activeChunkUploads.Lock()
|
||||||
|
if active, ok := activeChunkUploads.items[uploadID]; ok {
|
||||||
|
active.timer.Stop()
|
||||||
|
}
|
||||||
|
delete(activeChunkUploads.items, uploadID)
|
||||||
|
activeChunkUploads.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func discardActiveChunkUpload(uploadID, partFile string) error {
|
||||||
|
deleteActiveChunkUpload(uploadID)
|
||||||
|
if err := os.Remove(partFile); err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("remove upload temporary file failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func finalizeActiveChunkUpload(uploadID, partFile, dstFile string, overwrite bool) error {
|
||||||
|
if err := finalizeUploadedFile(partFile, dstFile, overwrite); err != nil {
|
||||||
|
if removeErr := discardActiveChunkUpload(uploadID, partFile); removeErr != nil {
|
||||||
|
return errors.Join(err, removeErr)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expireActiveChunkUpload(uploadID string, expiresAt time.Time) {
|
||||||
|
unlock := lockChunkUpload(uploadID)
|
||||||
|
defer unlock()
|
||||||
|
activeChunkUploads.Lock()
|
||||||
|
active, ok := activeChunkUploads.items[uploadID]
|
||||||
|
if !ok || !active.expiresAt.Equal(expiresAt) {
|
||||||
|
activeChunkUploads.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete(activeChunkUploads.items, uploadID)
|
||||||
|
activeChunkUploads.Unlock()
|
||||||
|
partFile := resumableUploadPartPath(active.upload.dstDir, uploadID)
|
||||||
|
if err := os.Remove(partFile); err != nil && !os.IsNotExist(err) {
|
||||||
|
global.LOG.Warnf("remove inactive upload part [%s] failed: %v", partFile, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeActiveResumableUploadPart(uploadID string) error {
|
||||||
|
unlock := lockChunkUpload(uploadID)
|
||||||
|
defer unlock()
|
||||||
|
upload, ok := loadActiveChunkUpload(uploadID)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := os.Remove(resumableUploadPartPath(upload.dstDir, uploadID))
|
||||||
|
if err == nil || os.IsNotExist(err) {
|
||||||
|
deleteActiveChunkUpload(uploadID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadCompletedChunkUpload(uploadID string) (completedChunkUpload, bool) {
|
||||||
|
completedChunkUploads.RLock()
|
||||||
|
completed, ok := completedChunkUploads.items[uploadID]
|
||||||
|
completedChunkUploads.RUnlock()
|
||||||
|
return completed, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func markChunkUploadCompleted(uploadID string, completed completedChunkUpload) {
|
||||||
|
completedChunkUploads.Lock()
|
||||||
|
completedChunkUploads.items[uploadID] = completed
|
||||||
|
completedChunkUploads.Unlock()
|
||||||
|
time.AfterFunc(10*time.Minute, func() {
|
||||||
|
completedChunkUploads.Lock()
|
||||||
|
delete(completedChunkUploads.items, uploadID)
|
||||||
|
completedChunkUploads.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeResumableUploadChunk(chunk resumableUploadChunk, chunkData []byte) error {
|
||||||
|
unlock := lockChunkUpload(chunk.UploadID)
|
||||||
|
defer unlock()
|
||||||
|
if chunkUploadCancelled(chunk.UploadID) {
|
||||||
|
return errChunkUploadCancelled
|
||||||
|
}
|
||||||
|
if chunk.UploadID == "" || filepath.Base(chunk.UploadID) != chunk.UploadID || strings.ContainsAny(chunk.UploadID, `/\`) {
|
||||||
|
return invalidChunkUploadError("invalid upload ID")
|
||||||
|
}
|
||||||
|
if chunk.Filename == "" || filepath.Base(chunk.Filename) != chunk.Filename || strings.ContainsAny(chunk.Filename, `/\`) {
|
||||||
|
return invalidChunkUploadError("invalid filename")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(chunk.DstDir) == "" {
|
||||||
|
return invalidChunkUploadError("upload destination is required")
|
||||||
|
}
|
||||||
|
dstDir := filepath.Clean(strings.TrimSpace(chunk.DstDir))
|
||||||
|
|
||||||
|
if chunk.ChunkCount <= 0 || chunk.ChunkIndex < 0 || chunk.ChunkIndex >= chunk.ChunkCount {
|
||||||
|
return invalidChunkUploadError("invalid chunk index")
|
||||||
|
}
|
||||||
|
if chunk.FileSize <= 0 || chunk.Offset < 0 || chunk.Offset > chunk.FileSize {
|
||||||
|
return invalidChunkUploadError("invalid upload offset")
|
||||||
|
}
|
||||||
|
chunkEnd := chunk.Offset + int64(len(chunkData))
|
||||||
|
if chunkEnd > chunk.FileSize {
|
||||||
|
return invalidChunkUploadError("chunk exceeds file size")
|
||||||
|
}
|
||||||
|
if chunk.ChunkIndex+1 == chunk.ChunkCount {
|
||||||
|
if chunkEnd != chunk.FileSize {
|
||||||
|
return invalidChunkUploadError("final chunk does not match file size")
|
||||||
|
}
|
||||||
|
} else if chunkEnd >= chunk.FileSize {
|
||||||
|
return invalidChunkUploadError("non-final chunk reaches file size")
|
||||||
|
}
|
||||||
|
if completed, ok := loadCompletedChunkUpload(chunk.UploadID); ok {
|
||||||
|
if completed.dstDir == dstDir && completed.filename == chunk.Filename && completed.fileSize == chunk.FileSize {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return invalidChunkUploadError("upload ID has already completed another file")
|
||||||
|
}
|
||||||
|
upload := completedChunkUpload{dstDir: dstDir, filename: chunk.Filename, fileSize: chunk.FileSize}
|
||||||
|
if err := registerActiveChunkUpload(chunk.UploadID, upload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mode, err := files.GetParentMode(dstDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = os.MkdirAll(dstDir, mode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dstDirInfo, err := os.Stat(dstDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !dstDirInfo.IsDir() {
|
||||||
|
return invalidChunkUploadError(fmt.Sprintf("upload destination [%s] is not a directory", dstDir))
|
||||||
|
}
|
||||||
|
|
||||||
|
dstFile := filepath.Join(dstDir, chunk.Filename)
|
||||||
|
partFile := resumableUploadPartPath(dstDir, chunk.UploadID)
|
||||||
|
if dstFile == partFile {
|
||||||
|
return invalidChunkUploadError("filename conflicts with upload temporary file")
|
||||||
|
}
|
||||||
|
fileMode := dstDirInfo.Mode().Perm()
|
||||||
|
ownerInfo := dstDirInfo
|
||||||
|
if dstInfo, statErr := os.Stat(dstFile); statErr == nil {
|
||||||
|
if !chunk.Overwrite {
|
||||||
|
if err := discardActiveChunkUpload(chunk.UploadID, partFile); err != nil {
|
||||||
|
return errors.Join(os.ErrExist, err)
|
||||||
|
}
|
||||||
|
return os.ErrExist
|
||||||
|
}
|
||||||
|
fileMode = dstInfo.Mode().Perm()
|
||||||
|
ownerInfo = dstInfo
|
||||||
|
} else if !os.IsNotExist(statErr) {
|
||||||
|
return statErr
|
||||||
|
}
|
||||||
|
|
||||||
|
part, err := os.OpenFile(partFile, os.O_CREATE|os.O_RDWR, fileMode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
partClosed := false
|
||||||
|
defer func() {
|
||||||
|
if !partClosed {
|
||||||
|
_ = part.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if stat, statErr := part.Stat(); statErr != nil {
|
||||||
|
return statErr
|
||||||
|
} else if chunk.Offset > stat.Size() {
|
||||||
|
return invalidChunkUploadError(fmt.Sprintf("unexpected upload offset %d, current size is %d", chunk.Offset, stat.Size()))
|
||||||
|
} else if chunk.Offset < stat.Size() && chunkEnd > stat.Size() {
|
||||||
|
if err = part.Truncate(chunk.Offset); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err = part.WriteAt(chunkData, chunk.Offset); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if chunk.ChunkIndex+1 != chunk.ChunkCount {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
partInfo, err := part.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if partInfo.Size() != chunk.FileSize {
|
||||||
|
return invalidChunkUploadError(fmt.Sprintf("uploaded file size mismatch: expected %d, got %d", chunk.FileSize, partInfo.Size()))
|
||||||
|
}
|
||||||
|
if err = part.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
partClosed = true
|
||||||
|
if err = os.Chmod(partFile, fileMode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if stat, ok := ownerInfo.Sys().(*syscall.Stat_t); ok {
|
||||||
|
if err = os.Chown(partFile, int(stat.Uid), int(stat.Gid)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if chunkUploadCancelled(chunk.UploadID) {
|
||||||
|
return errChunkUploadCancelled
|
||||||
|
}
|
||||||
|
if err = finalizeActiveChunkUpload(chunk.UploadID, partFile, dstFile, chunk.Overwrite); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
markChunkUploadCompleted(chunk.UploadID, upload)
|
||||||
|
deleteActiveChunkUpload(chunk.UploadID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int, overwrite bool) error {
|
func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int, overwrite bool) error {
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = os.RemoveAll(fileDir)
|
_ = os.RemoveAll(fileDir)
|
||||||
@@ -871,6 +1245,10 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
|||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if chunkCount <= 0 || chunkIndex < 0 || chunkIndex >= chunkCount {
|
||||||
|
helper.BadRequest(c, errors.New("invalid chunk index"))
|
||||||
|
return
|
||||||
|
}
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
tmpDir := path.Join(global.Dir.TmpDir, "upload")
|
tmpDir := path.Join(global.Dir.TmpDir, "upload")
|
||||||
if !fileOp.Stat(tmpDir) {
|
if !fileOp.Stat(tmpDir) {
|
||||||
@@ -885,20 +1263,25 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
uploadID := strings.TrimSpace(c.PostForm("uploadID"))
|
uploadID := strings.TrimSpace(c.PostForm("uploadID"))
|
||||||
|
resumable := c.PostForm("fileSize") != "" || c.PostForm("offset") != ""
|
||||||
cancellable := uploadID != ""
|
cancellable := uploadID != ""
|
||||||
if cancellable && (filepath.Base(uploadID) != uploadID || strings.ContainsAny(uploadID, `/\\`)) {
|
if cancellable && (filepath.Base(uploadID) != uploadID || strings.ContainsAny(uploadID, `/\\`)) {
|
||||||
helper.BadRequest(c, errors.New("invalid upload ID"))
|
helper.BadRequest(c, errors.New("invalid upload ID"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if resumable && !cancellable {
|
||||||
|
helper.BadRequest(c, errors.New("upload ID is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
if !cancellable {
|
if !cancellable {
|
||||||
uploadID = filename
|
uploadID = filename
|
||||||
}
|
}
|
||||||
fileDir := filepath.Join(tmpDir, uploadID)
|
fileDir := filepath.Join(tmpDir, uploadID)
|
||||||
if cancellable && chunkUploadCancelled(uploadID) {
|
if cancellable && chunkUploadCancelled(uploadID) {
|
||||||
helper.BadRequest(c, errors.New("upload cancelled"))
|
helper.BadRequest(c, errChunkUploadCancelled)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if chunkIndex == 0 {
|
if !resumable && chunkIndex == 0 {
|
||||||
if fileOp.Stat(fileDir) {
|
if fileOp.Stat(fileDir) {
|
||||||
_ = fileOp.DeleteDir(fileDir)
|
_ = fileOp.DeleteDir(fileDir)
|
||||||
}
|
}
|
||||||
@@ -907,32 +1290,67 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
|||||||
filePath := filepath.Join(fileDir, filename)
|
filePath := filepath.Join(fileDir, filename)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if !resumable && err != nil {
|
||||||
_ = os.RemoveAll(fileDir)
|
_ = os.RemoveAll(fileDir)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
var (
|
chunkData, err := io.ReadAll(uploadFile)
|
||||||
emptyFile *os.File
|
|
||||||
chunkData []byte
|
|
||||||
)
|
|
||||||
|
|
||||||
emptyFile, err = os.Create(filePath)
|
|
||||||
if err != nil {
|
|
||||||
helper.BadRequest(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer emptyFile.Close()
|
|
||||||
|
|
||||||
chunkData, err = io.ReadAll(uploadFile)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
|
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if cancellable && chunkUploadCancelled(uploadID) {
|
if cancellable && chunkUploadCancelled(uploadID) {
|
||||||
err = errors.New("upload cancelled")
|
err = errChunkUploadCancelled
|
||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if resumable {
|
||||||
|
offset, parseErr := strconv.ParseInt(c.PostForm("offset"), 10, 64)
|
||||||
|
if parseErr != nil {
|
||||||
|
helper.BadRequest(c, parseErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileSize, parseErr := strconv.ParseInt(c.PostForm("fileSize"), 10, 64)
|
||||||
|
if parseErr != nil {
|
||||||
|
helper.BadRequest(c, parseErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
overwrite := true
|
||||||
|
if ow := c.PostForm("overwrite"); ow != "" {
|
||||||
|
overwrite, _ = strconv.ParseBool(ow)
|
||||||
|
}
|
||||||
|
err = writeResumableUploadChunk(resumableUploadChunk{
|
||||||
|
UploadID: uploadID,
|
||||||
|
Filename: filename,
|
||||||
|
DstDir: c.PostForm("path"),
|
||||||
|
ChunkIndex: chunkIndex,
|
||||||
|
ChunkCount: chunkCount,
|
||||||
|
Offset: offset,
|
||||||
|
FileSize: fileSize,
|
||||||
|
Overwrite: overwrite,
|
||||||
|
}, chunkData)
|
||||||
|
if err != nil {
|
||||||
|
uploadErr := buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err)
|
||||||
|
helper.ErrorWithDetailAndData(c, http.StatusInternalServerError, "ErrInternalServer", uploadErr, gin.H{
|
||||||
|
"retryable": isRetryableChunkUploadError(err),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if chunkIndex+1 == chunkCount {
|
||||||
|
cancelledChunkUploads.Lock()
|
||||||
|
delete(cancelledChunkUploads.ids, uploadID)
|
||||||
|
cancelledChunkUploads.Unlock()
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyFile, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer emptyFile.Close()
|
||||||
|
|
||||||
chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex))
|
chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex))
|
||||||
err = os.WriteFile(chunkPath, chunkData, constant.DirPerm)
|
err = os.WriteFile(chunkPath, chunkData, constant.DirPerm)
|
||||||
@@ -985,6 +1403,10 @@ func (b *BaseApi) StopChunkUpload(c *gin.Context) {
|
|||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := removeActiveResumableUploadPart(uploadID); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1211,7 +1633,7 @@ func (b *BaseApi) SearchFileShare(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
total, list, err := fileShareService.Page(req)
|
total, list, err := fileShareService.Page(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -1235,7 +1657,7 @@ func (b *BaseApi) GetFileShareDetail(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
info, err := fileShareService.GetByPath(req.Path)
|
info, err := fileShareService.GetByPath(req.Path, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -1254,7 +1676,7 @@ func (b *BaseApi) GetPublicFileShareInfo(c *gin.Context) {
|
|||||||
helper.BadRequest(c, errors.New("code is required"))
|
helper.BadRequest(c, errors.New("code is required"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
info, err := fileShareService.GetPublicByCode(code)
|
info, err := fileShareService.GetPublicByCode(code, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if be, ok := err.(buserr.BusinessError); ok {
|
if be, ok := err.(buserr.BusinessError); ok {
|
||||||
helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be)
|
helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be)
|
||||||
@@ -1307,7 +1729,7 @@ func (b *BaseApi) GetFileShareQRCode(c *gin.Context) {
|
|||||||
helper.BadRequest(c, errors.New("code is required"))
|
helper.BadRequest(c, errors.New("code is required"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err := fileShareService.GetByCode(code); err != nil {
|
if _, err := fileShareService.GetByCode(code, helper.IsDemoRequest(c)); err != nil {
|
||||||
if be, ok := err.(buserr.BusinessError); ok {
|
if be, ok := err.(buserr.BusinessError); ok {
|
||||||
helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be)
|
helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be)
|
||||||
return
|
return
|
||||||
@@ -1389,7 +1811,7 @@ func (b *BaseApi) CheckFileShare(c *gin.Context) {
|
|||||||
helper.BadRequest(c, errors.New("code is required"))
|
helper.BadRequest(c, errors.New("code is required"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := fileShareService.Check(code, password); err != nil {
|
if err := fileShareService.Check(code, password, helper.IsDemoRequest(c)); err != nil {
|
||||||
if be, ok := err.(buserr.BusinessError); ok {
|
if be, ok := err.(buserr.BusinessError); ok {
|
||||||
helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be)
|
helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be)
|
||||||
return
|
return
|
||||||
@@ -1414,7 +1836,7 @@ func (b *BaseApi) DownloadFileShare(c *gin.Context) {
|
|||||||
helper.BadRequest(c, errors.New("code is required"))
|
helper.BadRequest(c, errors.New("code is required"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
filePath, displayName, err := fileShareService.PrepareDownload(code, password)
|
filePath, displayName, err := fileShareService.PrepareDownload(code, password, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if be, ok := err.(buserr.BusinessError); ok {
|
if be, ok := err.(buserr.BusinessError); ok {
|
||||||
helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be)
|
helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be)
|
||||||
|
|||||||
@@ -20,7 +20,15 @@ func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := firewallService.LoadBaseInfo(req.Name)
|
var (
|
||||||
|
data dto.FirewallBaseInfo
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if req.Name == "forward" {
|
||||||
|
data, err = forwardingService.LoadBaseInfo()
|
||||||
|
} else {
|
||||||
|
data, err = firewallService.LoadBaseInfo(req.Name)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -43,7 +51,21 @@ func (b *BaseApi) SearchFirewallRule(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
total, list, err := firewallService.SearchWithPage(req)
|
var (
|
||||||
|
total int64
|
||||||
|
list interface{}
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if req.Type == "forward" {
|
||||||
|
total, list, err = forwardingService.SearchWithPage(dto.ForwardRuleSearch{
|
||||||
|
PageInfo: req.PageInfo,
|
||||||
|
Info: req.Info,
|
||||||
|
Status: req.Status,
|
||||||
|
Strategy: req.Strategy,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
total, list, err = firewallService.SearchWithPage(req)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -116,7 +138,7 @@ func (b *BaseApi) OperateForwardRule(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := firewallService.OperateForwardRule(req); err != nil {
|
if err := forwardingService.Operate(req); err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -313,7 +335,13 @@ func (b *BaseApi) OperateFilterChain(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := iptablesService.Operate(req); err != nil {
|
var err error
|
||||||
|
if req.Operate == "init-forward" {
|
||||||
|
err = forwardingService.Enable()
|
||||||
|
} else {
|
||||||
|
err = iptablesService.Operate(req)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ func (b *BaseApi) SearchFtp(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
total, list, err := ftpService.SearchWithPage(req)
|
total, list, err := ftpService.SearchWithPage(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -30,6 +30,16 @@ func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
|||||||
ctx.Abort()
|
ctx.Abort()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ErrorWithDetailAndData(ctx *gin.Context, code int, msgKey string, err error, data interface{}) {
|
||||||
|
res := dto.Response{
|
||||||
|
Code: code,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
res.Message = i18n.GetMsgWithDetail(msgKey, err.Error())
|
||||||
|
ctx.JSON(http.StatusOK, res)
|
||||||
|
ctx.Abort()
|
||||||
|
}
|
||||||
|
|
||||||
func InternalServer(ctx *gin.Context, err error) {
|
func InternalServer(ctx *gin.Context, err error) {
|
||||||
ErrorWithDetail(ctx, http.StatusInternalServerError, "ErrInternalServer", err)
|
ErrorWithDetail(ctx, http.StatusInternalServerError, "ErrInternalServer", err)
|
||||||
}
|
}
|
||||||
@@ -38,6 +48,10 @@ func BadRequest(ctx *gin.Context, err error) {
|
|||||||
ErrorWithDetail(ctx, http.StatusBadRequest, "ErrInvalidParams", err)
|
ErrorWithDetail(ctx, http.StatusBadRequest, "ErrInvalidParams", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsDemoRequest(ctx *gin.Context) bool {
|
||||||
|
return global.CONF.Base.IsDemo || ctx.GetHeader(constant.DemoModeHeader) == strconv.FormatBool(true)
|
||||||
|
}
|
||||||
|
|
||||||
func SuccessWithData(ctx *gin.Context, data interface{}) {
|
func SuccessWithData(ctx *gin.Context, data interface{}) {
|
||||||
if data == nil {
|
if data == nil {
|
||||||
data = gin.H{}
|
data = gin.H{}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ func (b *BaseApi) SearchHost(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
total, list, err := hostService.SearchWithPage(req)
|
total, list, err := hostService.SearchWithPage(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func (b *BaseApi) SearchRepo(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
total, list, err := imageRepoService.Page(req)
|
total, list, err := imageRepoService.Page(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func (b *BaseApi) PageMcpServers(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
list := mcpServerService.Page(req)
|
list := mcpServerService.Page(req, helper.IsDemoRequest(c))
|
||||||
helper.SuccessWithData(c, list)
|
helper.SuccessWithData(c, list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ func (b *BaseApi) SearchRuntimes(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
req.ReadOnly = helper.IsDemoRequest(c)
|
||||||
total, items, err := runtimeService.Page(req)
|
total, items, err := runtimeService.Page(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
@@ -132,7 +133,7 @@ func (b *BaseApi) GetRuntime(c *gin.Context) {
|
|||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res, err := runtimeService.Get(id)
|
res, err := runtimeService.Get(id, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -529,7 +530,7 @@ func (b *BaseApi) GetPHPContainerConfig(c *gin.Context) {
|
|||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data, err := runtimeService.GetPHPContainerConfig(id)
|
data, err := runtimeService.GetPHPContainerConfig(id, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ func (b *BaseApi) LoadBaseDir(c *gin.Context) {
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /settings/ssh/conn [get]
|
// @Router /settings/ssh/conn [get]
|
||||||
func (b *BaseApi) LoadLocalConn(c *gin.Context) {
|
func (b *BaseApi) LoadLocalConn(c *gin.Context) {
|
||||||
helper.SuccessWithData(c, settingService.GetLocalConn())
|
helper.SuccessWithData(c, settingService.GetLocalConn(helper.IsDemoRequest(c)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags System Setting
|
// @Tags System Setting
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func (b *BaseApi) SearchRootCert(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
total, data, err := sshService.SearchRootCerts(req)
|
total, data, err := sshService.SearchRootCerts(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func (b *BaseApi) PageTensorRTLLMs(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
list := tensorrtLLMService.Page(req)
|
list := tensorrtLLMService.Page(req, helper.IsDemoRequest(c))
|
||||||
helper.SuccessWithData(c, list)
|
helper.SuccessWithData(c, list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
|
|||||||
return nil, 0, 0, false
|
return nil, 0, 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if global.CONF.Base.IsDemo {
|
if helper.IsDemoRequest(c) {
|
||||||
if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) {
|
if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) {
|
||||||
return nil, 0, 0, false
|
return nil, 0, 0, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ func (b *BaseApi) GetHTTPSConfig(c *gin.Context) {
|
|||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res, err := websiteService.GetWebsiteHTTPS(id)
|
res, err := websiteService.GetWebsiteHTTPS(id, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -1080,7 +1080,7 @@ func (b *BaseApi) GetWebsiteResource(c *gin.Context) {
|
|||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res, err := websiteService.GetWebsiteResource(id)
|
res, err := websiteService.GetWebsiteResource(id, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func (b *BaseApi) PageWebsiteAcmeAccount(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
total, accounts, err := websiteAcmeAccountService.Page(req)
|
total, accounts, err := websiteAcmeAccountService.Page(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func (b *BaseApi) PageWebsiteCA(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
total, cas, err := websiteCAService.Page(req)
|
total, cas, err := websiteCAService.Page(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -70,7 +70,7 @@ func (b *BaseApi) GetWebsiteCA(c *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res, err := websiteCAService.GetCA(id)
|
res, err := websiteCAService.GetCA(id, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func (b *BaseApi) PageWebsiteDnsAccount(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
total, accounts, err := websiteDnsAccountService.Page(req)
|
total, accounts, err := websiteDnsAccountService.Page(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func (b *BaseApi) PageWebsiteSSL(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
total, accounts, err := websiteSSLService.Page(req)
|
total, accounts, err := websiteSSLService.Page(req, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -159,7 +159,7 @@ func (b *BaseApi) GetWebsiteSSLByWebsiteId(c *gin.Context) {
|
|||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
websiteSSL, err := websiteSSLService.GetWebsiteSSL(websiteId)
|
websiteSSL, err := websiteSSLService.GetWebsiteSSL(websiteId, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -181,7 +181,7 @@ func (b *BaseApi) GetWebsiteSSLById(c *gin.Context) {
|
|||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
websiteSSL, err := websiteSSLService.GetSSL(id)
|
websiteSSL, err := websiteSSLService.GetSSL(id, helper.IsDemoRequest(c))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,259 @@
|
|||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Page website templates
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.WebsiteTemplateSearch true "request"
|
||||||
|
// @Success 200 {object} dto.PageResult
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates/search [post]
|
||||||
|
func (b *BaseApi) PageWebsiteTemplate(c *gin.Context) {
|
||||||
|
var req request.WebsiteTemplateSearch
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
total, templates, err := websiteTemplateService.PageTemplate(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, dto.PageResult{
|
||||||
|
Total: total,
|
||||||
|
Items: templates,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Create website template
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.WebsiteTemplateCreate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建网站模板 [name]","formatEN":"Create website template [name]"}
|
||||||
|
func (b *BaseApi) CreateWebsiteTemplate(c *gin.Context) {
|
||||||
|
var req request.WebsiteTemplateCreate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := websiteTemplateService.CreateTemplate(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Update website template
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.WebsiteTemplateUpdate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates/update [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新网站模板 [name]","formatEN":"Update website template [name]"}
|
||||||
|
func (b *BaseApi) UpdateWebsiteTemplate(c *gin.Context) {
|
||||||
|
var req request.WebsiteTemplateUpdate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := websiteTemplateService.UpdateTemplate(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Delete website template
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.OperateByID true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates/del [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_templates","output_column":"name","output_value":"name"}],"formatZH":"删除网站模板 [name]","formatEN":"Delete website template [name]"}
|
||||||
|
func (b *BaseApi) DeleteWebsiteTemplate(c *gin.Context) {
|
||||||
|
var req dto.OperateByID
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := websiteTemplateService.DeleteTemplate(req.ID); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Get website template
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.OperateByID true "request"
|
||||||
|
// @Success 200 {object} response.WebsiteTemplateDTO
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates/get [post]
|
||||||
|
func (b *BaseApi) GetWebsiteTemplate(c *gin.Context) {
|
||||||
|
var req dto.OperateByID
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
template, err := websiteTemplateService.GetTemplate(req.ID)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, template)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Upload website template zip
|
||||||
|
// @Accept multipart/form-data
|
||||||
|
// @Param file formData file true "file"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates/upload [post]
|
||||||
|
func (b *BaseApi) UploadTemplateZip(c *gin.Context) {
|
||||||
|
fileHeader, err := c.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file, err := fileHeader.Open()
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
content, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filePath, variables, err := websiteTemplateService.SaveUploadZip(fileHeader.Filename, content)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, gin.H{"filePath": filePath, "variables": variables})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Preview website template
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.WebsitePreviewReq true "request"
|
||||||
|
// @Success 200 {object} response.WebsitePreviewDTO
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates/preview [post]
|
||||||
|
func (b *BaseApi) PreviewWebsiteTemplate(c *gin.Context) {
|
||||||
|
var req request.WebsitePreviewReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
preview, err := websiteTemplateService.Preview(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, preview)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Page website template outputs
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.WebsiteTemplateOutputSearch true "request"
|
||||||
|
// @Success 200 {object} dto.PageResult
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates/outputs/search [post]
|
||||||
|
func (b *BaseApi) PageWebsiteTemplateOutput(c *gin.Context) {
|
||||||
|
var req request.WebsiteTemplateOutputSearch
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
total, outputs, err := websiteTemplateService.PageOutput(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, dto.PageResult{
|
||||||
|
Total: total,
|
||||||
|
Items: outputs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Create website template output
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.WebsiteTemplateOutputCreate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates/outputs [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成模板产物 [name]","formatEN":"Generate template output [name]"}
|
||||||
|
func (b *BaseApi) CreateWebsiteTemplateOutput(c *gin.Context) {
|
||||||
|
var req request.WebsiteTemplateOutputCreate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := websiteTemplateService.CreateOutput(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Delete website template output
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.OperateByID true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates/outputs/del [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_template_outputs","output_column":"name","output_value":"name"}],"formatZH":"删除模板产物 [name]","formatEN":"Delete template output [name]"}
|
||||||
|
func (b *BaseApi) DeleteWebsiteTemplateOutput(c *gin.Context) {
|
||||||
|
var req dto.OperateByID
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := websiteTemplateService.DeleteOutput(req.ID); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Website Template
|
||||||
|
// @Summary Get website template output
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.OperateByID true "request"
|
||||||
|
// @Success 200 {object} response.WebsiteTemplateOutputDTO
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/templates/outputs/get [post]
|
||||||
|
func (b *BaseApi) GetWebsiteTemplateOutput(c *gin.Context) {
|
||||||
|
var req dto.OperateByID
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
output, err := websiteTemplateService.GetOutput(req.ID)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, output)
|
||||||
|
}
|
||||||
+79
-27
@@ -322,29 +322,31 @@ type AgentAccountModelDeleteReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountCreateReq struct {
|
type AgentAccountCreateReq struct {
|
||||||
Provider string `json:"provider" validate:"required"`
|
Provider string `json:"provider" validate:"required"`
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
APIKey string `json:"apiKey" validate:"required"`
|
APIKey string `json:"apiKey" validate:"required"`
|
||||||
RememberAPIKey bool `json:"rememberApiKey"`
|
RememberAPIKey bool `json:"rememberApiKey"`
|
||||||
BaseURL string `json:"baseURL"`
|
BaseURL string `json:"baseURL"`
|
||||||
Models []AgentAccountModel `json:"models"`
|
Models []AgentAccountModel `json:"models"`
|
||||||
APIType string `json:"apiType" validate:"required"`
|
APIType string `json:"apiType" validate:"required"`
|
||||||
AuthMode string `json:"authMode"`
|
AuthMode string `json:"authMode"`
|
||||||
VerifyModel string `json:"verifyModel"`
|
VerifyModel string `json:"verifyModel"`
|
||||||
Remark string `json:"remark"`
|
ValidateAvailability *bool `json:"validateAvailability"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountUpdateReq struct {
|
type AgentAccountUpdateReq struct {
|
||||||
ID uint `json:"id" validate:"required"`
|
ID uint `json:"id" validate:"required"`
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
APIKey string `json:"apiKey" validate:"required"`
|
APIKey string `json:"apiKey" validate:"required"`
|
||||||
RememberAPIKey bool `json:"rememberApiKey"`
|
RememberAPIKey bool `json:"rememberApiKey"`
|
||||||
BaseURL string `json:"baseURL"`
|
BaseURL string `json:"baseURL"`
|
||||||
APIType string `json:"apiType" validate:"required"`
|
APIType string `json:"apiType" validate:"required"`
|
||||||
AuthMode string `json:"authMode"`
|
AuthMode string `json:"authMode"`
|
||||||
VerifyModel string `json:"verifyModel"`
|
VerifyModel string `json:"verifyModel"`
|
||||||
Remark string `json:"remark"`
|
ValidateAvailability *bool `json:"validateAvailability"`
|
||||||
SyncAgents bool `json:"syncAgents"`
|
Remark string `json:"remark"`
|
||||||
|
SyncAgents bool `json:"syncAgents"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountVerifyReq struct {
|
type AgentAccountVerifyReq struct {
|
||||||
@@ -363,6 +365,8 @@ type AgentAccountDeleteReq struct {
|
|||||||
type AgentAccountSearch struct {
|
type AgentAccountSearch struct {
|
||||||
PageInfo
|
PageInfo
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
|
APIType string `json:"apiType"`
|
||||||
|
TextOnly bool `json:"textOnly"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,11 +398,13 @@ type ProviderModelInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProviderAPIInfo struct {
|
type ProviderAPIInfo struct {
|
||||||
APIType string `json:"apiType"`
|
APIType string `json:"apiType"`
|
||||||
BaseURL string `json:"baseUrl"`
|
BaseURL string `json:"baseUrl"`
|
||||||
EditableBaseURL bool `json:"editableBaseUrl"`
|
EditableBaseURL bool `json:"editableBaseUrl"`
|
||||||
DefaultAuthMode string `json:"defaultAuthMode"`
|
SupportsModelDiscovery bool `json:"supportsModelDiscovery"`
|
||||||
AuthModes []string `json:"authModes"`
|
DefaultAuthMode string `json:"defaultAuthMode"`
|
||||||
|
AuthModes []string `json:"authModes"`
|
||||||
|
Models []ProviderModelInfo `json:"models"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProviderInfo struct {
|
type ProviderInfo struct {
|
||||||
@@ -598,6 +604,52 @@ type AgentPluginStatus struct {
|
|||||||
Upgradable bool `json:"upgradable"`
|
Upgradable bool `json:"upgradable"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AgentPluginsReq struct {
|
||||||
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPluginSearchReq struct {
|
||||||
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
|
Keyword string `json:"keyword" validate:"required,max=100"`
|
||||||
|
Limit int `json:"limit" validate:"omitempty,min=1,max=100"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPluginMarketInstallReq struct {
|
||||||
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
|
Package string `json:"package" validate:"required,max=200"`
|
||||||
|
Version string `json:"version" validate:"required,max=100"`
|
||||||
|
TaskID string `json:"taskID" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPluginOperateReq struct {
|
||||||
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
|
PluginID string `json:"pluginId" validate:"required,max=200"`
|
||||||
|
Operate string `json:"operate" validate:"required,oneof=enable disable update uninstall"`
|
||||||
|
TaskID string `json:"taskID" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPluginItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Origin string `json:"origin"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPluginSearchItem struct {
|
||||||
|
Package string `json:"package"`
|
||||||
|
PluginID string `json:"pluginId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
VerificationTier string `json:"verificationTier"`
|
||||||
|
Categories []string `json:"categories"`
|
||||||
|
Official bool `json:"official"`
|
||||||
|
Downloads int64 `json:"downloads"`
|
||||||
|
Score float64 `json:"score"`
|
||||||
|
}
|
||||||
|
|
||||||
type AgentDiscordConfigUpdateReq struct {
|
type AgentDiscordConfigUpdateReq struct {
|
||||||
AgentID uint `json:"agentId" validate:"required"`
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
@@ -678,9 +730,9 @@ type AgentSecurityConfig struct {
|
|||||||
|
|
||||||
type AgentOtherConfigUpdateReq struct {
|
type AgentOtherConfigUpdateReq struct {
|
||||||
AgentID uint `json:"agentId" validate:"required"`
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
UserTimezone string `json:"userTimezone" validate:"required"`
|
UserTimezone string `json:"userTimezone"`
|
||||||
BrowserEnabled bool `json:"browserEnabled"`
|
BrowserEnabled bool `json:"browserEnabled"`
|
||||||
NPMRegistry string `json:"npmRegistry" validate:"required"`
|
NPMRegistry string `json:"npmRegistry"`
|
||||||
DashboardUsername string `json:"dashboardUsername"`
|
DashboardUsername string `json:"dashboardUsername"`
|
||||||
DashboardPassword string `json:"dashboardPassword"`
|
DashboardPassword string `json:"dashboardPassword"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,8 +113,10 @@ type DiskDTO struct {
|
|||||||
|
|
||||||
type AlertLogSearch struct {
|
type AlertLogSearch struct {
|
||||||
PageInfo
|
PageInfo
|
||||||
Count uint `json:"count"`
|
Count uint `json:"count"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
StartTime time.Time `json:"startTime"`
|
||||||
|
EndTime time.Time `json:"endTime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertLogDTO struct {
|
type AlertLogDTO struct {
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ package dto
|
|||||||
|
|
||||||
type SearchWithPage struct {
|
type SearchWithPage struct {
|
||||||
PageInfo
|
PageInfo
|
||||||
Info string `json:"info"`
|
Info string `json:"info"`
|
||||||
|
ExcludeAppStore bool `json:"excludeAppStore"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchPageWithType struct {
|
type SearchPageWithType struct {
|
||||||
|
|||||||
@@ -300,6 +300,7 @@ type ComposeInfo struct {
|
|||||||
ConfigFile string `json:"configFile"`
|
ConfigFile string `json:"configFile"`
|
||||||
Workdir string `json:"workdir"`
|
Workdir string `json:"workdir"`
|
||||||
ComposeFileExists bool `json:"composeFileExists"`
|
ComposeFileExists bool `json:"composeFileExists"`
|
||||||
|
IsPinned bool `json:"isPinned"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Containers []ComposeContainer `json:"containers"`
|
Containers []ComposeContainer `json:"containers"`
|
||||||
Env string `json:"env"`
|
Env string `json:"env"`
|
||||||
@@ -314,6 +315,7 @@ type ComposeContainer struct {
|
|||||||
type ComposeCreate struct {
|
type ComposeCreate struct {
|
||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
DirName string `json:"dirName"`
|
||||||
From string `json:"from" validate:"required,oneof=edit path template"`
|
From string `json:"from" validate:"required,oneof=edit path template"`
|
||||||
File string `json:"file"`
|
File string `json:"file"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
@@ -337,6 +339,10 @@ type ComposeUpdate struct {
|
|||||||
Env string `json:"env"`
|
Env string `json:"env"`
|
||||||
ForcePull bool `json:"forcePull"`
|
ForcePull bool `json:"forcePull"`
|
||||||
}
|
}
|
||||||
|
type ComposePin struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
IsPinned bool `json:"isPinned"`
|
||||||
|
}
|
||||||
type ComposeLogClean struct {
|
type ComposeLogClean struct {
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Path string `json:"path" validate:"required"`
|
Path string `json:"path" validate:"required"`
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ type SearchRecord struct {
|
|||||||
|
|
||||||
type Record struct {
|
type Record struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
|
CronjobID uint `json:"cronjobID"`
|
||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
StartTime string `json:"startTime"`
|
StartTime string `json:"startTime"`
|
||||||
Records string `json:"records"`
|
Records string `json:"records"`
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ type DiskInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GPUInfo struct {
|
type GPUInfo struct {
|
||||||
|
Type string `json:"type"`
|
||||||
Index uint `json:"index"`
|
Index uint `json:"index"`
|
||||||
ProductName string `json:"productName"`
|
ProductName string `json:"productName"`
|
||||||
GPUUtil string `json:"gpuUtil"`
|
GPUUtil string `json:"gpuUtil"`
|
||||||
|
|||||||
@@ -35,19 +35,6 @@ type PortRuleOperate struct {
|
|||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ForwardRuleOperate struct {
|
|
||||||
ForceDelete bool `json:"forceDelete"`
|
|
||||||
Rules []struct {
|
|
||||||
Operation string `json:"operation" validate:"required,oneof=add remove"`
|
|
||||||
Num string `json:"num"`
|
|
||||||
Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"`
|
|
||||||
Interface string `json:"interface"`
|
|
||||||
Port string `json:"port" validate:"required"`
|
|
||||||
TargetIP string `json:"targetIP"`
|
|
||||||
TargetPort string `json:"targetPort" validate:"required"`
|
|
||||||
} `json:"rules"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateFirewallDescription struct {
|
type UpdateFirewallDescription struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Chain string `json:"chain"`
|
Chain string `json:"chain"`
|
||||||
@@ -86,7 +73,7 @@ type BatchRuleOperate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type IptablesOp struct {
|
type IptablesOp struct {
|
||||||
Name string `json:"name" validate:"required,oneof=1PANEL_INPUT 1PANEL_OUTPUT 1PANEL_BASIC"`
|
Name string `json:"name" validate:"required,oneof=1PANEL_INPUT 1PANEL_OUTPUT 1PANEL_BASIC 1PANEL_FORWARD"`
|
||||||
Operate string `json:"operate" validate:"required,oneof=init-base init-forward init-advance bind-base unbind-base bind unbind"`
|
Operate string `json:"operate" validate:"required,oneof=init-base init-forward init-advance bind-base unbind-base bind unbind"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type ForwardRuleSearch struct {
|
||||||
|
PageInfo
|
||||||
|
Info string `json:"info"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Strategy string `json:"strategy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForwardRule preserves the existing firewall search response shape while
|
||||||
|
// keeping forwarding data separate from the filter client model.
|
||||||
|
type ForwardRule struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Chain string `json:"chain"`
|
||||||
|
Family string `json:"family"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Port string `json:"port"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
Strategy string `json:"strategy"`
|
||||||
|
|
||||||
|
Num string `json:"num"`
|
||||||
|
TargetIP string `json:"targetIP"`
|
||||||
|
TargetPort string `json:"targetPort"`
|
||||||
|
Interface string `json:"interface"`
|
||||||
|
|
||||||
|
UsedStatus string `json:"usedStatus"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForwardRuleOperate struct {
|
||||||
|
ForceDelete bool `json:"forceDelete"`
|
||||||
|
Rules []ForwardRuleOperation `json:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForwardRuleOperation struct {
|
||||||
|
Operation string `json:"operation" validate:"required,oneof=add remove"`
|
||||||
|
Num string `json:"num"`
|
||||||
|
Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"`
|
||||||
|
Interface string `json:"interface"`
|
||||||
|
Port string `json:"port" validate:"required"`
|
||||||
|
TargetIP string `json:"targetIP"`
|
||||||
|
TargetPort string `json:"targetPort" validate:"required"`
|
||||||
|
}
|
||||||
@@ -49,6 +49,7 @@ type GPUChartHide struct {
|
|||||||
GPU bool `json:"gpu"`
|
GPU bool `json:"gpu"`
|
||||||
Memory bool `json:"memory"`
|
Memory bool `json:"memory"`
|
||||||
Power bool `json:"power"`
|
Power bool `json:"power"`
|
||||||
|
PowerLimit bool `json:"powerLimit"`
|
||||||
Temperature bool `json:"temperature"`
|
Temperature bool `json:"temperature"`
|
||||||
Speed bool `json:"speed"`
|
Speed bool `json:"speed"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,11 +62,13 @@ type AppInstalledSearch struct {
|
|||||||
All bool `json:"all"`
|
All bool `json:"all"`
|
||||||
Sync bool `json:"sync"`
|
Sync bool `json:"sync"`
|
||||||
CheckUpdate bool `json:"checkUpdate"`
|
CheckUpdate bool `json:"checkUpdate"`
|
||||||
|
ReadOnly bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppInstalledInfo struct {
|
type AppInstalledInfo struct {
|
||||||
Key string `json:"key" validate:"required"`
|
Key string `json:"key" validate:"required"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
ReadOnly bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppBackupSearch struct {
|
type AppBackupSearch struct {
|
||||||
|
|||||||
@@ -131,6 +131,11 @@ type FileMove struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Cover bool `json:"cover"`
|
Cover bool `json:"cover"`
|
||||||
CoverPaths []string `json:"coverPaths"`
|
CoverPaths []string `json:"coverPaths"`
|
||||||
|
TaskID string `json:"taskID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileMoveStopReq struct {
|
||||||
|
TaskID string `json:"taskID" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileDownload struct {
|
type FileDownload struct {
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import (
|
|||||||
|
|
||||||
type RuntimeSearch struct {
|
type RuntimeSearch struct {
|
||||||
dto.PageInfo
|
dto.PageInfo
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
ReadOnly bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuntimeCreate struct {
|
type RuntimeCreate struct {
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ type WebsiteCreate struct {
|
|||||||
|
|
||||||
SiteDir string `json:"siteDir"`
|
SiteDir string `json:"siteDir"`
|
||||||
|
|
||||||
|
TemplateOutputID uint `json:"templateOutputID"`
|
||||||
|
|
||||||
RuntimeConfig
|
RuntimeConfig
|
||||||
FtpConfig
|
FtpConfig
|
||||||
DataBaseConfig
|
DataBaseConfig
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package request
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebsiteTemplateSearch struct {
|
||||||
|
dto.PageInfo
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteTemplateCreate struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Type string `json:"type" validate:"required,oneof=single multi"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
FilePath string `json:"filePath"`
|
||||||
|
Variables string `json:"variables"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteTemplateUpdate struct {
|
||||||
|
ID uint `json:"id" validate:"required"`
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Type string `json:"type" validate:"required,oneof=single multi"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
FilePath string `json:"filePath"`
|
||||||
|
Variables string `json:"variables"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteTemplateOutputSearch struct {
|
||||||
|
dto.PageInfo
|
||||||
|
TemplateID uint `json:"templateID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteTemplateOutputCreate struct {
|
||||||
|
TemplateID uint `json:"templateID" validate:"required"`
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
VariableValues map[string]string `json:"variableValues"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsitePreviewReq struct {
|
||||||
|
TemplateID uint `json:"templateID" validate:"required"`
|
||||||
|
VariableValues map[string]string `json:"variableValues"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package response
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebsiteTemplateDTO struct {
|
||||||
|
model.WebsiteTemplate
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteTemplateOutputDTO struct {
|
||||||
|
model.WebsiteTemplateOutput
|
||||||
|
TemplateName string `json:"templateName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsitePreviewDTO struct {
|
||||||
|
HTML string `json:"html"`
|
||||||
|
}
|
||||||
@@ -31,4 +31,5 @@ type BackupRecord struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
Args string `gorm:"not null;default:''" json:"args"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type ComposeTemplate struct {
|
|||||||
type Compose struct {
|
type Compose struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
|
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
|
IsPinned bool `json:"isPinned"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,6 @@ type Ftp struct {
|
|||||||
Status string `gorm:"not null" json:"status"`
|
Status string `gorm:"not null" json:"status"`
|
||||||
Path string `gorm:"not null" json:"path"`
|
Path string `gorm:"not null" json:"path"`
|
||||||
Description string `gorm:"not null" json:"description"`
|
Description string `gorm:"not null" json:"description"`
|
||||||
|
UID uint `gorm:"column:uid;not null;default:1000" json:"-"`
|
||||||
|
GID uint `gorm:"column:gid;not null;default:1000" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type WebsiteTemplate struct {
|
||||||
|
BaseModel
|
||||||
|
Name string `gorm:"not null" json:"name"`
|
||||||
|
Type string `gorm:"not null" json:"type"` // single | multi
|
||||||
|
Content string `gorm:"type:longtext" json:"content"`
|
||||||
|
FilePath string `json:"filePath"`
|
||||||
|
Variables string `gorm:"type:text" json:"variables"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w WebsiteTemplate) TableName() string {
|
||||||
|
return "website_templates"
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteTemplateOutput struct {
|
||||||
|
BaseModel
|
||||||
|
Name string `gorm:"not null" json:"name"`
|
||||||
|
TemplateID uint `gorm:"not null" json:"templateID"`
|
||||||
|
TemplateType string `json:"templateType"`
|
||||||
|
VariableValues string `gorm:"type:text" json:"variableValues"`
|
||||||
|
OutputPath string `json:"outputPath"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w WebsiteTemplateOutput) TableName() string {
|
||||||
|
return "website_template_outputs"
|
||||||
|
}
|
||||||
+143
-13
@@ -10,8 +10,10 @@ type APIConfig struct {
|
|||||||
APIType string
|
APIType string
|
||||||
BaseURL string
|
BaseURL string
|
||||||
EditableBaseURL bool
|
EditableBaseURL bool
|
||||||
|
DiscoverModels bool
|
||||||
DefaultAuthMode string
|
DefaultAuthMode string
|
||||||
AuthModes []string
|
AuthModes []string
|
||||||
|
Models []Model
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -27,6 +29,7 @@ type Model struct {
|
|||||||
type Meta struct {
|
type Meta struct {
|
||||||
Key string
|
Key string
|
||||||
DisplayName string
|
DisplayName string
|
||||||
|
DisplayNameKey string
|
||||||
Sort uint
|
Sort uint
|
||||||
DefaultAPIType string
|
DefaultAPIType string
|
||||||
APIConfigs []APIConfig
|
APIConfigs []APIConfig
|
||||||
@@ -37,26 +40,27 @@ type Meta struct {
|
|||||||
var catalog = map[string]Meta{
|
var catalog = map[string]Meta{
|
||||||
"custom": {
|
"custom": {
|
||||||
Key: "custom", DisplayName: "Custom", Sort: 10, DefaultAPIType: "openai-completions", EnvKey: "CUSTOM_API_KEY",
|
Key: "custom", DisplayName: "Custom", Sort: 10, DefaultAPIType: "openai-completions", EnvKey: "CUSTOM_API_KEY",
|
||||||
APIConfigs: editableAPIConfigs("openai-completions", "openai-responses", "anthropic-messages"),
|
APIConfigs: editableAPIConfigs(true, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "openai-embeddings"),
|
||||||
},
|
},
|
||||||
"ollama": {
|
"ollama": {
|
||||||
Key: "ollama", DisplayName: "Ollama", Sort: 15, DefaultAPIType: "openai-responses",
|
Key: "ollama", DisplayName: "Ollama", Sort: 15, DefaultAPIType: "openai-responses",
|
||||||
APIConfigs: editableAPIConfigs("openai-responses", "openai-completions"),
|
APIConfigs: editableAPIConfigs(false, "openai-responses", "openai-completions", "openai-embeddings"),
|
||||||
},
|
},
|
||||||
"vllm": {
|
"vllm": {
|
||||||
Key: "vllm", DisplayName: "vLLM", Sort: 20, DefaultAPIType: "openai-completions", EnvKey: "VLLM_API_KEY",
|
Key: "vllm", DisplayName: "vLLM", Sort: 20, DefaultAPIType: "openai-completions", EnvKey: "VLLM_API_KEY",
|
||||||
APIConfigs: editableAPIConfigs("openai-completions", "openai-responses", "anthropic-messages"),
|
APIConfigs: editableAPIConfigs(false, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "openai-embeddings"),
|
||||||
},
|
},
|
||||||
"deepseek": {
|
"deepseek": {
|
||||||
Key: "deepseek", DisplayName: "DeepSeek", Sort: 25, DefaultAPIType: "openai-completions", EnvKey: "DEEPSEEK_API_KEY",
|
Key: "deepseek", DisplayName: "DeepSeek", Sort: 25, DefaultAPIType: "openai-completions", EnvKey: "DEEPSEEK_API_KEY",
|
||||||
APIConfigs: []APIConfig{
|
APIConfigs: []APIConfig{
|
||||||
{APIType: "openai-completions", BaseURL: "https://api.deepseek.com/v1"},
|
{APIType: "openai-completions", BaseURL: "https://api.deepseek.com"},
|
||||||
|
{APIType: "openai-responses", BaseURL: "https://api.deepseek.com"},
|
||||||
anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey),
|
anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey),
|
||||||
},
|
},
|
||||||
Models: []Model{{ID: "deepseek-v4-flash", Name: "deepseek-v4-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
|
Models: []Model{{ID: "deepseek-v4-flash", Name: "deepseek-v4-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
|
||||||
},
|
},
|
||||||
"bailian-coding-plan": {
|
"bailian-coding-plan": {
|
||||||
Key: "bailian-coding-plan", DisplayName: "阿里云百炼 Coding Plan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY",
|
Key: "bailian-coding-plan", DisplayNameKey: "AIProviderBailianCodingPlan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY",
|
||||||
APIConfigs: []APIConfig{
|
APIConfigs: []APIConfig{
|
||||||
{APIType: "openai-completions", BaseURL: "https://coding.dashscope.aliyuncs.com/v1"},
|
{APIType: "openai-completions", BaseURL: "https://coding.dashscope.aliyuncs.com/v1"},
|
||||||
anthropicAPIConfig("https://coding.dashscope.aliyuncs.com/apps/anthropic", AuthModeBearer),
|
anthropicAPIConfig("https://coding.dashscope.aliyuncs.com/apps/anthropic", AuthModeBearer),
|
||||||
@@ -75,7 +79,7 @@ var catalog = map[string]Meta{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"ark-coding-plan": {
|
"ark-coding-plan": {
|
||||||
Key: "ark-coding-plan", DisplayName: "方舟 Coding Plan", Sort: 35, DefaultAPIType: "openai-completions", EnvKey: "ARK_API_KEY",
|
Key: "ark-coding-plan", DisplayNameKey: "AIProviderArkCodingPlan", Sort: 35, DefaultAPIType: "openai-completions", EnvKey: "ARK_API_KEY",
|
||||||
APIConfigs: []APIConfig{
|
APIConfigs: []APIConfig{
|
||||||
{APIType: "openai-completions", BaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3"},
|
{APIType: "openai-completions", BaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3"},
|
||||||
anthropicAPIConfig("https://ark.cn-beijing.volces.com/api/coding", AuthModeBearer),
|
anthropicAPIConfig("https://ark.cn-beijing.volces.com/api/coding", AuthModeBearer),
|
||||||
@@ -88,14 +92,18 @@ var catalog = map[string]Meta{
|
|||||||
},
|
},
|
||||||
"zai": {
|
"zai": {
|
||||||
Key: "zai", DisplayName: "Z.ai", Sort: 40, DefaultAPIType: "openai-completions", EnvKey: "ZAI_API_KEY",
|
Key: "zai", DisplayName: "Z.ai", Sort: 40, DefaultAPIType: "openai-completions", EnvKey: "ZAI_API_KEY",
|
||||||
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true}},
|
APIConfigs: []APIConfig{
|
||||||
Models: []Model{{ID: "glm-5", Name: "GLM-5"}, {ID: "glm-4.7", Name: "GLM-4.7"}, {ID: "glm-4.7-flash", Name: "GLM-4.7-Flash"}, {ID: "glm-4.7-flashx", Name: "GLM-4.7-FlashX"}},
|
{APIType: "openai-completions", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true},
|
||||||
|
{APIType: "openai-images", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true},
|
||||||
|
},
|
||||||
|
Models: []Model{{ID: "glm-5", Name: "GLM-5"}, {ID: "glm-4.7", Name: "GLM-4.7"}, {ID: "glm-4.7-flash", Name: "GLM-4.7-Flash"}, {ID: "glm-4.7-flashx", Name: "GLM-4.7-FlashX"}},
|
||||||
},
|
},
|
||||||
"minimax": {
|
"minimax": {
|
||||||
Key: "minimax", DisplayName: "MiniMax (CN)", Sort: 45, DefaultAPIType: "anthropic-messages", EnvKey: "MINIMAX_API_KEY",
|
Key: "minimax", DisplayName: "MiniMax (CN)", Sort: 45, DefaultAPIType: "anthropic-messages", EnvKey: "MINIMAX_API_KEY",
|
||||||
APIConfigs: []APIConfig{
|
APIConfigs: []APIConfig{
|
||||||
anthropicAPIConfig("https://api.minimaxi.com/anthropic", AuthModeXAPIKey, AuthModeBearer),
|
anthropicAPIConfig("https://api.minimaxi.com/anthropic", AuthModeXAPIKey, AuthModeBearer),
|
||||||
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"},
|
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"},
|
||||||
|
{APIType: "minimax-images", BaseURL: "https://api.minimaxi.com"},
|
||||||
},
|
},
|
||||||
Models: []Model{{ID: "MiniMax-M3", Name: "MiniMax M3"}, {ID: "MiniMax-M2.7", Name: "MiniMax M2.7"}, {ID: "MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"}},
|
Models: []Model{{ID: "MiniMax-M3", Name: "MiniMax M3"}, {ID: "MiniMax-M2.7", Name: "MiniMax M2.7"}, {ID: "MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"}},
|
||||||
},
|
},
|
||||||
@@ -103,9 +111,10 @@ var catalog = map[string]Meta{
|
|||||||
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY",
|
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY",
|
||||||
APIConfigs: []APIConfig{
|
APIConfigs: []APIConfig{
|
||||||
{APIType: "openai-completions", BaseURL: "https://api.xiaomimimo.com/v1"},
|
{APIType: "openai-completions", BaseURL: "https://api.xiaomimimo.com/v1"},
|
||||||
|
{APIType: "openai-responses", BaseURL: "https://api.xiaomimimo.com/v1"},
|
||||||
anthropicAPIConfig("https://api.xiaomimimo.com/anthropic", AuthModeBearer),
|
anthropicAPIConfig("https://api.xiaomimimo.com/anthropic", AuthModeBearer),
|
||||||
},
|
},
|
||||||
Models: []Model{{ID: "mimo-v2-flash", Name: "Xiaomi MiMo V2 Flash"}, {ID: "mimo-v2-pro", Name: "Xiaomi MiMo V2 Pro"}, {ID: "mimo-v2-omni", Name: "Xiaomi MiMo V2 Omni"}},
|
Models: []Model{{ID: "mimo-v2.5", Name: "Xiaomi MiMo V2.5"}, {ID: "mimo-v2.5-pro", Name: "Xiaomi MiMo V2.5 Pro"}},
|
||||||
},
|
},
|
||||||
"kimi": {
|
"kimi": {
|
||||||
Key: "kimi", DisplayName: "Kimi (CN)", Sort: 50, DefaultAPIType: "openai-completions", EnvKey: "KIMI_API_KEY",
|
Key: "kimi", DisplayName: "Kimi (CN)", Sort: 50, DefaultAPIType: "openai-completions", EnvKey: "KIMI_API_KEY",
|
||||||
@@ -122,13 +131,21 @@ var catalog = map[string]Meta{
|
|||||||
APIConfigs: []APIConfig{
|
APIConfigs: []APIConfig{
|
||||||
{APIType: "openai-responses", BaseURL: "https://api.openai.com/v1"},
|
{APIType: "openai-responses", BaseURL: "https://api.openai.com/v1"},
|
||||||
{APIType: "openai-completions", BaseURL: "https://api.openai.com/v1"},
|
{APIType: "openai-completions", BaseURL: "https://api.openai.com/v1"},
|
||||||
|
{APIType: "openai-images", BaseURL: "https://api.openai.com/v1"},
|
||||||
|
{APIType: "openai-embeddings", BaseURL: "https://api.openai.com/v1", Models: []Model{
|
||||||
|
{ID: "text-embedding-3-small", Name: "text-embedding-3-small"},
|
||||||
|
{ID: "text-embedding-3-large", Name: "text-embedding-3-large"},
|
||||||
|
}},
|
||||||
},
|
},
|
||||||
Models: []Model{{ID: "gpt-5.4", Name: "gpt-5.4"}, {ID: "gpt-5.4-pro", Name: "gpt-5.4-pro"}, {ID: "gpt-5.4-mini", Name: "gpt-5.4-mini"}, {ID: "gpt-5.4-nano", Name: "gpt-5.4-nano"}},
|
Models: []Model{{ID: "gpt-5.4", Name: "gpt-5.4"}, {ID: "gpt-5.4-pro", Name: "gpt-5.4-pro"}, {ID: "gpt-5.4-mini", Name: "gpt-5.4-mini"}, {ID: "gpt-5.4-nano", Name: "gpt-5.4-nano"}},
|
||||||
},
|
},
|
||||||
"openrouter": {
|
"openrouter": {
|
||||||
Key: "openrouter", DisplayName: "OpenRouter", Sort: 56, DefaultAPIType: "openai-completions", EnvKey: "OPENROUTER_API_KEY",
|
Key: "openrouter", DisplayName: "OpenRouter", Sort: 56, DefaultAPIType: "openai-completions", EnvKey: "OPENROUTER_API_KEY",
|
||||||
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://openrouter.ai/api/v1"}},
|
APIConfigs: []APIConfig{
|
||||||
Models: []Model{{ID: "openrouter/free", Name: "openrouter/free"}, {ID: "openrouter/auto", Name: "openrouter/auto"}},
|
{APIType: "openai-completions", BaseURL: "https://openrouter.ai/api/v1"},
|
||||||
|
{APIType: "openrouter-images", BaseURL: "https://openrouter.ai"},
|
||||||
|
},
|
||||||
|
Models: []Model{{ID: "openrouter/free", Name: "openrouter/free"}, {ID: "openrouter/auto", Name: "openrouter/auto"}},
|
||||||
},
|
},
|
||||||
"anthropic": {
|
"anthropic": {
|
||||||
Key: "anthropic", DisplayName: "Anthropic", Sort: 60, DefaultAPIType: "anthropic-messages", EnvKey: "ANTHROPIC_API_KEY",
|
Key: "anthropic", DisplayName: "Anthropic", Sort: 60, DefaultAPIType: "anthropic-messages", EnvKey: "ANTHROPIC_API_KEY",
|
||||||
@@ -145,9 +162,62 @@ var catalog = map[string]Meta{
|
|||||||
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://api.moonshot.ai/v1"}},
|
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://api.moonshot.ai/v1"}},
|
||||||
Models: []Model{{ID: "kimi-k2.5", Name: "Kimi K2.5"}, {ID: "kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"}},
|
Models: []Model{{ID: "kimi-k2.5", Name: "Kimi K2.5"}, {ID: "kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"}},
|
||||||
},
|
},
|
||||||
|
"bailian": {
|
||||||
|
Key: "bailian", DisplayNameKey: "AIProviderBailian", Sort: 31, DefaultAPIType: "openai-completions", EnvKey: "DASHSCOPE_API_KEY",
|
||||||
|
APIConfigs: []APIConfig{
|
||||||
|
{
|
||||||
|
APIType: "openai-completions", BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||||
|
DiscoverModels: true,
|
||||||
|
Models: []Model{{ID: "qwen3.7-plus", Name: "qwen3.7-plus"}, {ID: "qwen3.6-plus", Name: "qwen3.6-plus"}, {ID: "qwen3.6-flash", Name: "qwen3.6-flash"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
APIType: "openai-responses", BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||||
|
DiscoverModels: true,
|
||||||
|
Models: []Model{{ID: "qwen3.7-plus", Name: "qwen3.7-plus"}, {ID: "qwen3.6-plus", Name: "qwen3.6-plus"}, {ID: "qwen3.6-flash", Name: "qwen3.6-flash"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
APIType: "anthropic-messages", BaseURL: "https://dashscope.aliyuncs.com/apps/anthropic",
|
||||||
|
DefaultAuthMode: AuthModeBearer,
|
||||||
|
AuthModes: []string{AuthModeBearer},
|
||||||
|
Models: []Model{{ID: "qwen3.7-plus", Name: "qwen3.7-plus"}, {ID: "qwen3.6-plus", Name: "qwen3.6-plus"}, {ID: "qwen3.6-flash", Name: "qwen3.6-flash"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
APIType: "dashscope-images", BaseURL: "https://dashscope.aliyuncs.com",
|
||||||
|
Models: []Model{
|
||||||
|
{ID: "qwen-image-2.0-pro", Name: "qwen-image-2.0-pro"},
|
||||||
|
{ID: "qwen-image-2.0", Name: "qwen-image-2.0"},
|
||||||
|
{ID: "wan2.7-image-pro", Name: "wan2.7-image-pro"},
|
||||||
|
{ID: "wan2.7-image", Name: "wan2.7-image"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ark": {
|
||||||
|
Key: "ark", DisplayNameKey: "AIProviderArk", Sort: 36, DefaultAPIType: "openai-completions", EnvKey: "ARK_API_KEY",
|
||||||
|
APIConfigs: []APIConfig{
|
||||||
|
{
|
||||||
|
APIType: "openai-completions", BaseURL: "https://ark.cn-beijing.volces.com/api/v3",
|
||||||
|
DiscoverModels: true,
|
||||||
|
Models: []Model{{ID: "doubao-seed-2-0-pro-260215", Name: "doubao-seed-2-0-pro-260215"}, {ID: "doubao-seed-2-0-lite-260215", Name: "doubao-seed-2-0-lite-260215"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
APIType: "openai-responses", BaseURL: "https://ark.cn-beijing.volces.com/api/v3",
|
||||||
|
DiscoverModels: true,
|
||||||
|
Models: []Model{{ID: "doubao-seed-2-0-pro-260215", Name: "doubao-seed-2-0-pro-260215"}, {ID: "doubao-seed-2-0-lite-260215", Name: "doubao-seed-2-0-lite-260215"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
APIType: "openai-images", BaseURL: "https://ark.cn-beijing.volces.com/api/v3",
|
||||||
|
Models: []Model{
|
||||||
|
{ID: "doubao-seedream-5-0-260128", Name: "doubao-seedream-5-0-260128"},
|
||||||
|
{ID: "doubao-seedream-5-0-lite-260128", Name: "doubao-seedream-5-0-lite-260128"},
|
||||||
|
{ID: "doubao-seedream-4-5-251128", Name: "doubao-seedream-4-5-251128"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func editableAPIConfigs(apiTypes ...string) []APIConfig {
|
func editableAPIConfigs(discoverModels bool, apiTypes ...string) []APIConfig {
|
||||||
configs := make([]APIConfig, 0, len(apiTypes))
|
configs := make([]APIConfig, 0, len(apiTypes))
|
||||||
for _, apiType := range apiTypes {
|
for _, apiType := range apiTypes {
|
||||||
if apiType == "anthropic-messages" {
|
if apiType == "anthropic-messages" {
|
||||||
@@ -156,7 +226,11 @@ func editableAPIConfigs(apiTypes ...string) []APIConfig {
|
|||||||
configs = append(configs, config)
|
configs = append(configs, config)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
configs = append(configs, APIConfig{APIType: apiType, EditableBaseURL: true})
|
configs = append(configs, APIConfig{
|
||||||
|
APIType: apiType,
|
||||||
|
EditableBaseURL: true,
|
||||||
|
DiscoverModels: discoverModels && (apiType == "openai-completions" || apiType == "openai-responses"),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return configs
|
return configs
|
||||||
}
|
}
|
||||||
@@ -203,12 +277,38 @@ func FindAPIConfig(key, apiType string) (APIConfig, bool) {
|
|||||||
}
|
}
|
||||||
for _, config := range meta.APIConfigs {
|
for _, config := range meta.APIConfigs {
|
||||||
if config.APIType == target {
|
if config.APIType == target {
|
||||||
|
config.AuthModes = append([]string(nil), config.AuthModes...)
|
||||||
|
config.Models = append([]Model(nil), config.Models...)
|
||||||
return config, true
|
return config, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return APIConfig{}, false
|
return APIConfig{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DefaultModels(key, apiType string) []Model {
|
||||||
|
meta, ok := catalog[key]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
target := strings.TrimSpace(apiType)
|
||||||
|
if target == "" {
|
||||||
|
target = meta.DefaultAPIType
|
||||||
|
}
|
||||||
|
for _, config := range meta.APIConfigs {
|
||||||
|
if config.APIType != target {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(config.Models) > 0 {
|
||||||
|
return append([]Model(nil), config.Models...)
|
||||||
|
}
|
||||||
|
if IsImageAPIType(config.APIType) || IsEmbeddingAPIType(config.APIType) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return append([]Model(nil), meta.Models...)
|
||||||
|
}
|
||||||
|
|
||||||
func ResolveAuthMode(provider, apiType, requested string) (string, error) {
|
func ResolveAuthMode(provider, apiType, requested string) (string, error) {
|
||||||
config, ok := FindAPIConfig(provider, apiType)
|
config, ok := FindAPIConfig(provider, apiType)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -264,6 +364,9 @@ func ResolveBaseURL(key, apiType, requested string) (string, error) {
|
|||||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||||
return "", fmt.Errorf("invalid base url")
|
return "", fmt.Errorf("invalid base url")
|
||||||
}
|
}
|
||||||
|
if key == "custom" && (IsImageAPIType(config.APIType) || IsEmbeddingAPIType(config.APIType)) {
|
||||||
|
return baseURL, nil
|
||||||
|
}
|
||||||
parsed.Path = normalizeEndpointPath(config.APIType, parsed.Path)
|
parsed.Path = normalizeEndpointPath(config.APIType, parsed.Path)
|
||||||
parsed.RawQuery = ""
|
parsed.RawQuery = ""
|
||||||
parsed.Fragment = ""
|
parsed.Fragment = ""
|
||||||
@@ -271,6 +374,9 @@ func ResolveBaseURL(key, apiType, requested string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func normalizeEndpointPath(apiType, value string) string {
|
func normalizeEndpointPath(apiType, value string) string {
|
||||||
|
if IsImageAPIType(apiType) {
|
||||||
|
return strings.TrimRight(value, "/")
|
||||||
|
}
|
||||||
path := strings.TrimRight(value, "/")
|
path := strings.TrimRight(value, "/")
|
||||||
suffixes := []string{}
|
suffixes := []string{}
|
||||||
switch apiType {
|
switch apiType {
|
||||||
@@ -280,6 +386,8 @@ func normalizeEndpointPath(apiType, value string) string {
|
|||||||
suffixes = []string{"/responses"}
|
suffixes = []string{"/responses"}
|
||||||
case "anthropic-messages":
|
case "anthropic-messages":
|
||||||
suffixes = []string{"/v1/messages", "/messages"}
|
suffixes = []string{"/v1/messages", "/messages"}
|
||||||
|
case "openai-embeddings":
|
||||||
|
suffixes = []string{"/v1/embeddings", "/embeddings"}
|
||||||
}
|
}
|
||||||
for _, suffix := range suffixes {
|
for _, suffix := range suffixes {
|
||||||
if strings.HasSuffix(strings.ToLower(path), suffix) {
|
if strings.HasSuffix(strings.ToLower(path), suffix) {
|
||||||
@@ -289,6 +397,19 @@ func normalizeEndpointPath(apiType, value string) string {
|
|||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsEmbeddingAPIType(apiType string) bool {
|
||||||
|
return apiType == "openai-embeddings"
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsImageAPIType(apiType string) bool {
|
||||||
|
switch apiType {
|
||||||
|
case "openai-images", "dashscope-images", "minimax-images", "openrouter-images":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func EnvKey(key string) string {
|
func EnvKey(key string) string {
|
||||||
meta, ok := catalog[key]
|
meta, ok := catalog[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -305,6 +426,14 @@ func DisplayName(key string) string {
|
|||||||
return meta.DisplayName
|
return meta.DisplayName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DisplayNameKey(key string) string {
|
||||||
|
meta, ok := catalog[key]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return meta.DisplayNameKey
|
||||||
|
}
|
||||||
|
|
||||||
func NormalizeModelID(provider, modelID string) string {
|
func NormalizeModelID(provider, modelID string) string {
|
||||||
target := strings.TrimLeft(strings.TrimSpace(modelID), "/")
|
target := strings.TrimLeft(strings.TrimSpace(modelID), "/")
|
||||||
for _, prefix := range legacyModelPrefixes[provider] {
|
for _, prefix := range legacyModelPrefixes[provider] {
|
||||||
@@ -344,6 +473,7 @@ func cloneMeta(meta Meta) Meta {
|
|||||||
for index, config := range meta.APIConfigs {
|
for index, config := range meta.APIConfigs {
|
||||||
clone.APIConfigs[index] = config
|
clone.APIConfigs[index] = config
|
||||||
clone.APIConfigs[index].AuthModes = append([]string(nil), config.AuthModes...)
|
clone.APIConfigs[index].AuthModes = append([]string(nil), config.AuthModes...)
|
||||||
|
clone.APIConfigs[index].Models = append([]Model(nil), config.Models...)
|
||||||
}
|
}
|
||||||
clone.Models = append([]Model(nil), meta.Models...)
|
clone.Models = append([]Model(nil), meta.Models...)
|
||||||
return clone
|
return clone
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,11 +55,20 @@ func buildModelDiscoveryURL(baseURL string) string {
|
|||||||
base = normalizeEndpointPath(apiType, base)
|
base = normalizeEndpointPath(apiType, base)
|
||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case strings.HasSuffix(base, "/v1/models"):
|
case strings.HasSuffix(base, "/models"):
|
||||||
return base
|
return base
|
||||||
case strings.HasSuffix(base, "/v1"):
|
case hasAPIVersionSuffix(base):
|
||||||
return base + "/models"
|
return base + "/models"
|
||||||
default:
|
default:
|
||||||
return base + "/v1/models"
|
return base + "/v1/models"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hasAPIVersionSuffix(value string) bool {
|
||||||
|
segment := value[strings.LastIndex(value, "/")+1:]
|
||||||
|
if len(segment) < 2 || segment[0] != 'v' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err := strconv.Atoi(segment[1:])
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ func BuildOpenClawProviderPatch(provider, modelName, apiType, authMode, baseURL,
|
|||||||
if _, ok := FindAPIConfig(provider, resolvedAPIType); !ok {
|
if _, ok := FindAPIConfig(provider, resolvedAPIType); !ok {
|
||||||
resolvedAPIType = DefaultAPIType(provider)
|
resolvedAPIType = DefaultAPIType(provider)
|
||||||
}
|
}
|
||||||
|
if IsImageAPIType(resolvedAPIType) || IsEmbeddingAPIType(resolvedAPIType) {
|
||||||
|
return nil, fmt.Errorf("api type %s does not support text generation", resolvedAPIType)
|
||||||
|
}
|
||||||
resolvedAuthMode, err := ResolveAuthMode(provider, resolvedAPIType, authMode)
|
resolvedAuthMode, err := ResolveAuthMode(provider, resolvedAPIType, authMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -64,7 +64,10 @@ func VerifyAccount(provider, apiType, authMode, baseURL, apiKey, model string) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model string) VerifyRequest {
|
func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model string) VerifyRequest {
|
||||||
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
|
baseURL = strings.TrimSpace(baseURL)
|
||||||
|
if provider != "custom" || !IsImageAPIType(apiType) {
|
||||||
|
baseURL = strings.TrimRight(baseURL, "/")
|
||||||
|
}
|
||||||
headers := map[string]string{"Content-Type": "application/json"}
|
headers := map[string]string{"Content-Type": "application/json"}
|
||||||
request := VerifyRequest{Method: http.MethodPost, Headers: headers}
|
request := VerifyRequest{Method: http.MethodPost, Headers: headers}
|
||||||
|
|
||||||
@@ -78,6 +81,32 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch apiType {
|
switch apiType {
|
||||||
|
case "openai-embeddings":
|
||||||
|
request.URL = embeddingVerifyURL(baseURL)
|
||||||
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
|
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "ping"})
|
||||||
|
case "openai-images":
|
||||||
|
request.URL = imageVerifyURL(provider, baseURL, "/images/generations")
|
||||||
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
|
request.Body = mustJSON(map[string]interface{}{"model": model, "prompt": "test", "n": 1, "response_format": "url"})
|
||||||
|
case "dashscope-images":
|
||||||
|
request.URL = imageVerifyURL(provider, baseURL, "/api/v1/services/aigc/multimodal-generation/generation")
|
||||||
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
|
request.Body = mustJSON(map[string]interface{}{
|
||||||
|
"model": model,
|
||||||
|
"input": map[string]interface{}{"messages": []map[string]interface{}{
|
||||||
|
{"role": "user", "content": []map[string]string{{"text": "test"}}},
|
||||||
|
}},
|
||||||
|
"parameters": map[string]interface{}{"n": 1},
|
||||||
|
})
|
||||||
|
case "minimax-images":
|
||||||
|
request.URL = imageVerifyURL(provider, baseURL, "/v1/image_generation")
|
||||||
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
|
request.Body = mustJSON(map[string]interface{}{"model": model, "prompt": "test", "n": 1, "response_format": "url"})
|
||||||
|
case "openrouter-images":
|
||||||
|
request.URL = imageVerifyURL(provider, baseURL, "/api/v1/images")
|
||||||
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
|
request.Body = mustJSON(map[string]interface{}{"model": model, "prompt": "test", "n": 1, "response_format": "url"})
|
||||||
case "anthropic-messages":
|
case "anthropic-messages":
|
||||||
request.URL = baseURL + "/v1/messages"
|
request.URL = baseURL + "/v1/messages"
|
||||||
if authMode == AuthModeBearer {
|
if authMode == AuthModeBearer {
|
||||||
@@ -106,6 +135,24 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
|
|||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func embeddingVerifyURL(baseURL string) string {
|
||||||
|
lowerBaseURL := strings.ToLower(baseURL)
|
||||||
|
if strings.HasSuffix(lowerBaseURL, "/embeddings") {
|
||||||
|
return baseURL
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(lowerBaseURL, "/v1") {
|
||||||
|
return baseURL + "/embeddings"
|
||||||
|
}
|
||||||
|
return baseURL + "/v1/embeddings"
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageVerifyURL(provider, baseURL, endpoint string) string {
|
||||||
|
if provider == "custom" || strings.HasSuffix(strings.ToLower(baseURL), endpoint) {
|
||||||
|
return baseURL
|
||||||
|
}
|
||||||
|
return baseURL + endpoint
|
||||||
|
}
|
||||||
|
|
||||||
func verifyHTTPError(statusCode int, body []byte) string {
|
func verifyHTTPError(statusCode int, body []byte) string {
|
||||||
message := strings.TrimSpace(string(body))
|
message := strings.TrimSpace(string(body))
|
||||||
var payload verifyErrorResponse
|
var payload verifyErrorResponse
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type IAgentAccountRepo interface {
|
|||||||
Save(account *model.AgentAccount) error
|
Save(account *model.AgentAccount) error
|
||||||
DeleteByID(id uint) error
|
DeleteByID(id uint) error
|
||||||
List(opts ...DBOption) ([]model.AgentAccount, error)
|
List(opts ...DBOption) ([]model.AgentAccount, error)
|
||||||
CountByProviders(providers []string) (map[string]int64, error)
|
CountTextByProviders(providers []string) (map[string]int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIAgentAccountRepo() IAgentAccountRepo {
|
func NewIAgentAccountRepo() IAgentAccountRepo {
|
||||||
@@ -67,7 +67,7 @@ func (a AgentAccountRepo) List(opts ...DBOption) ([]model.AgentAccount, error) {
|
|||||||
return accounts, nil
|
return accounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentAccountRepo) CountByProviders(providers []string) (map[string]int64, error) {
|
func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]int64, error) {
|
||||||
normalizedProviders := normalizeProviders(providers)
|
normalizedProviders := normalizeProviders(providers)
|
||||||
counts := make(map[string]int64, len(normalizedProviders))
|
counts := make(map[string]int64, len(normalizedProviders))
|
||||||
for _, provider := range normalizedProviders {
|
for _, provider := range normalizedProviders {
|
||||||
@@ -86,6 +86,7 @@ func (a AgentAccountRepo) CountByProviders(providers []string) (map[string]int64
|
|||||||
Model(&model.AgentAccount{}).
|
Model(&model.AgentAccount{}).
|
||||||
Select("provider, COUNT(*) as count").
|
Select("provider, COUNT(*) as count").
|
||||||
Where("provider IN ?", normalizedProviders).
|
Where("provider IN ?", normalizedProviders).
|
||||||
|
Scopes(WithTextAPIType()).
|
||||||
Group("provider").
|
Group("provider").
|
||||||
Scan(&rows).Error; err != nil {
|
Scan(&rows).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ func WithByName(name string) DBOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithByPath(path string) DBOption {
|
||||||
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
|
return g.Where("path = ?", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithByAddr(addr string) DBOption {
|
func WithByAddr(addr string) DBOption {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
return g.Where("addr = ?", addr)
|
return g.Where("addr = ?", addr)
|
||||||
@@ -94,6 +100,18 @@ func WithByProvider(provider string) DBOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithByAPIType(apiType string) DBOption {
|
||||||
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
|
return g.Where("api_type = ?", apiType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTextAPIType() DBOption {
|
||||||
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
|
return g.Where("api_type NOT LIKE ? AND api_type <> ?", "%-images", "openai-embeddings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func WithByModel(model string) DBOption {
|
func WithByModel(model string) DBOption {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
if len(model) == 0 {
|
if len(model) == 0 {
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ func (u *MonitorRepo) CreateMonitorBase(model model.MonitorBase) error {
|
|||||||
return global.MonitorDB.Create(&model).Error
|
return global.MonitorDB.Create(&model).Error
|
||||||
}
|
}
|
||||||
func (s *MonitorRepo) BatchCreateMonitorGPU(list []model.MonitorGPU) error {
|
func (s *MonitorRepo) BatchCreateMonitorGPU(list []model.MonitorGPU) error {
|
||||||
|
if len(list) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return global.GPUMonitorDB.CreateInBatches(&list, len(list)).Error
|
return global.GPUMonitorDB.CreateInBatches(&list, len(list)).Error
|
||||||
}
|
}
|
||||||
func (u *MonitorRepo) BatchCreateMonitorIO(ioList []model.MonitorIO) error {
|
func (u *MonitorRepo) BatchCreateMonitorIO(ioList []model.MonitorIO) error {
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IWebsiteTemplateRepo interface {
|
||||||
|
Page(page, size int, opts ...DBOption) (int64, []model.WebsiteTemplate, error)
|
||||||
|
GetFirst(opts ...DBOption) (*model.WebsiteTemplate, error)
|
||||||
|
List(opts ...DBOption) ([]model.WebsiteTemplate, error)
|
||||||
|
Create(template *model.WebsiteTemplate) error
|
||||||
|
Save(template *model.WebsiteTemplate) error
|
||||||
|
DeleteBy(opts ...DBOption) error
|
||||||
|
WithName(name string) DBOption
|
||||||
|
WithType(templateType string) DBOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIWebsiteTemplateRepo() IWebsiteTemplateRepo {
|
||||||
|
return &WebsiteTemplateRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteTemplateRepo struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateRepo) WithName(name string) DBOption {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("name like ?", "%"+name+"%")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateRepo) WithType(templateType string) DBOption {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("type = ?", templateType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteTemplate, error) {
|
||||||
|
var templates []model.WebsiteTemplate
|
||||||
|
db := getDb(opts...).Model(&model.WebsiteTemplate{})
|
||||||
|
count := int64(0)
|
||||||
|
db = db.Count(&count)
|
||||||
|
err := db.Limit(size).Offset(size * (page - 1)).Find(&templates).Error
|
||||||
|
return count, templates, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateRepo) GetFirst(opts ...DBOption) (*model.WebsiteTemplate, error) {
|
||||||
|
var template model.WebsiteTemplate
|
||||||
|
db := getDb(opts...).Model(&model.WebsiteTemplate{})
|
||||||
|
if err := db.First(&template).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &template, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateRepo) List(opts ...DBOption) ([]model.WebsiteTemplate, error) {
|
||||||
|
var templates []model.WebsiteTemplate
|
||||||
|
err := getDb(opts...).Model(&model.WebsiteTemplate{}).Find(&templates).Error
|
||||||
|
return templates, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateRepo) Create(template *model.WebsiteTemplate) error {
|
||||||
|
return getDb().Create(template).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateRepo) Save(template *model.WebsiteTemplate) error {
|
||||||
|
return getDb().Save(template).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateRepo) DeleteBy(opts ...DBOption) error {
|
||||||
|
return getDb(opts...).Delete(&model.WebsiteTemplate{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
type IWebsiteTemplateOutputRepo interface {
|
||||||
|
Page(page, size int, opts ...DBOption) (int64, []model.WebsiteTemplateOutput, error)
|
||||||
|
GetFirst(opts ...DBOption) (*model.WebsiteTemplateOutput, error)
|
||||||
|
List(opts ...DBOption) ([]model.WebsiteTemplateOutput, error)
|
||||||
|
Create(output *model.WebsiteTemplateOutput) error
|
||||||
|
Save(output *model.WebsiteTemplateOutput) error
|
||||||
|
DeleteBy(opts ...DBOption) error
|
||||||
|
WithByTemplateID(templateID uint) DBOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIWebsiteTemplateOutputRepo() IWebsiteTemplateOutputRepo {
|
||||||
|
return &WebsiteTemplateOutputRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteTemplateOutputRepo struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateOutputRepo) WithByTemplateID(templateID uint) DBOption {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("template_id = ?", templateID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateOutputRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteTemplateOutput, error) {
|
||||||
|
var outputs []model.WebsiteTemplateOutput
|
||||||
|
db := getDb(opts...).Model(&model.WebsiteTemplateOutput{})
|
||||||
|
count := int64(0)
|
||||||
|
db = db.Count(&count)
|
||||||
|
err := db.Limit(size).Offset(size * (page - 1)).Find(&outputs).Error
|
||||||
|
return count, outputs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateOutputRepo) GetFirst(opts ...DBOption) (*model.WebsiteTemplateOutput, error) {
|
||||||
|
var output model.WebsiteTemplateOutput
|
||||||
|
db := getDb(opts...).Model(&model.WebsiteTemplateOutput{})
|
||||||
|
if err := db.First(&output).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateOutputRepo) List(opts ...DBOption) ([]model.WebsiteTemplateOutput, error) {
|
||||||
|
var outputs []model.WebsiteTemplateOutput
|
||||||
|
err := getDb(opts...).Model(&model.WebsiteTemplateOutput{}).Find(&outputs).Error
|
||||||
|
return outputs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateOutputRepo) Create(output *model.WebsiteTemplateOutput) error {
|
||||||
|
return getDb().Create(output).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateOutputRepo) Save(output *model.WebsiteTemplateOutput) error {
|
||||||
|
return getDb().Save(output).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteTemplateOutputRepo) DeleteBy(opts ...DBOption) error {
|
||||||
|
return getDb(opts...).Delete(&model.WebsiteTemplateOutput{}).Error
|
||||||
|
}
|
||||||
+83
-29
@@ -38,7 +38,7 @@ type IAgentService interface {
|
|||||||
BatchUpgrade(req dto.AgentBatchUpgradeReq) ([]dto.AgentBatchUpgradeResult, error)
|
BatchUpgrade(req dto.AgentBatchUpgradeReq) ([]dto.AgentBatchUpgradeResult, error)
|
||||||
BatchInstallSkill(req dto.AgentBatchSkillInstallReq) ([]dto.AgentBatchSkillInstallResult, error)
|
BatchInstallSkill(req dto.AgentBatchSkillInstallReq) ([]dto.AgentBatchSkillInstallResult, error)
|
||||||
BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBatchOperateResult, error)
|
BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBatchOperateResult, error)
|
||||||
Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error)
|
Page(req dto.SearchWithPage, readOnly bool) (int64, []dto.AgentItem, error)
|
||||||
DeleteCheck(req dto.AgentIDReq) ([]dto.AppResource, error)
|
DeleteCheck(req dto.AgentIDReq) ([]dto.AppResource, error)
|
||||||
Delete(req dto.AgentDeleteReq) error
|
Delete(req dto.AgentDeleteReq) error
|
||||||
ResetToken(req dto.AgentTokenResetReq) error
|
ResetToken(req dto.AgentTokenResetReq) error
|
||||||
@@ -76,7 +76,7 @@ type IAgentService interface {
|
|||||||
CreateAccount(req dto.AgentAccountCreateReq) error
|
CreateAccount(req dto.AgentAccountCreateReq) error
|
||||||
UpdateAccount(req dto.AgentAccountUpdateReq) error
|
UpdateAccount(req dto.AgentAccountUpdateReq) error
|
||||||
SyncAgentsByAccount(account *model.AgentAccount) error
|
SyncAgentsByAccount(account *model.AgentAccount) error
|
||||||
PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error)
|
PageAccounts(req dto.AgentAccountSearch, readOnly ...bool) (int64, []dto.AgentAccountInfo, error)
|
||||||
CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error)
|
CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error)
|
||||||
GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error)
|
GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error)
|
||||||
DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error)
|
DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error)
|
||||||
@@ -105,6 +105,10 @@ type IAgentService interface {
|
|||||||
UpgradePlugin(req dto.AgentPluginUpgradeReq) error
|
UpgradePlugin(req dto.AgentPluginUpgradeReq) error
|
||||||
UninstallPlugin(req dto.AgentPluginUninstallReq) error
|
UninstallPlugin(req dto.AgentPluginUninstallReq) error
|
||||||
CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error)
|
CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error)
|
||||||
|
ListPlugins(req dto.AgentPluginsReq) ([]dto.AgentPluginItem, error)
|
||||||
|
SearchPlugins(req dto.AgentPluginSearchReq) ([]dto.AgentPluginSearchItem, error)
|
||||||
|
InstallMarketPlugin(req dto.AgentPluginMarketInstallReq) error
|
||||||
|
OperatePlugin(req dto.AgentPluginOperateReq) error
|
||||||
ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error
|
ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +181,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
var allowedOrigins []string
|
var allowedOrigins []string
|
||||||
var account *model.AgentAccount
|
var account *model.AgentAccount
|
||||||
var installHooks *appInstallHooks
|
var installHooks *appInstallHooks
|
||||||
var hermesAuth hermesDashboardAuth
|
var dashboardAuth agentDashboardAuth
|
||||||
|
|
||||||
if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
|
if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
|
||||||
if req.AccountID == 0 {
|
if req.AccountID == 0 {
|
||||||
@@ -222,15 +226,17 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else if agentType == constant.AppHermesAgent {
|
} else if agentType == constant.AppHermesAgent {
|
||||||
hermesAuth = normalizeHermesDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||||
installHooks = &appInstallHooks{
|
installHooks = &appInstallHooks{
|
||||||
AfterCopyData: func(appInstall *model.AppInstall) error {
|
AfterCopyData: func(appInstall *model.AppInstall) error {
|
||||||
if err := prepareHermesInstallFiles(appInstall, account, storedModel); err != nil {
|
if err := prepareHermesInstallFiles(appInstall, account, storedModel); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return writeHermesDashboardAuthEnv(path.Join(appInstall.GetPath(), ".env"), hermesAuth, false)
|
return writeAgentDashboardAuthEnv(appInstall.GetEnvPath(), agentType, dashboardAuth, false)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
} else if agentType == constant.AppCopaw {
|
||||||
|
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
@@ -250,9 +256,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
params["API_KEY"] = apiKey
|
params["API_KEY"] = apiKey
|
||||||
params["OPENCLAW_GATEWAY_TOKEN"] = token
|
params["OPENCLAW_GATEWAY_TOKEN"] = token
|
||||||
}
|
}
|
||||||
if agentType == constant.AppHermesAgent {
|
if usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType); ok {
|
||||||
params[hermesDashboardUsernameEnvKey] = hermesAuth.Username
|
params[usernameKey] = dashboardAuth.Username
|
||||||
params[hermesDashboardPasswordEnvKey] = hermesAuth.Password
|
params[passwordKey] = dashboardAuth.Password
|
||||||
|
if agentType == constant.AppCopaw {
|
||||||
|
params[qwenPawAuthEnabledEnvKey] = "true"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" {
|
if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" {
|
||||||
@@ -736,7 +745,7 @@ func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion st
|
|||||||
params["PANEL_APP_PORT_HTTP"] = webUIPort
|
params["PANEL_APP_PORT_HTTP"] = webUIPort
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error) {
|
func (a AgentService) Page(req dto.SearchWithPage, readOnly bool) (int64, []dto.AgentItem, error) {
|
||||||
var opts []repo.DBOption
|
var opts []repo.DBOption
|
||||||
if strings.TrimSpace(req.Info) != "" {
|
if strings.TrimSpace(req.Info) != "" {
|
||||||
opts = append(opts, repo.WithByLikeName(req.Info))
|
opts = append(opts, repo.WithByLikeName(req.Info))
|
||||||
@@ -751,11 +760,19 @@ func (a AgentService) Page(req dto.SearchWithPage) (int64, []dto.AgentItem, erro
|
|||||||
appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(item.AppInstallID))
|
appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(item.AppInstallID))
|
||||||
appInstalls = append(appInstalls, appInstall)
|
appInstalls = append(appInstalls, appInstall)
|
||||||
}
|
}
|
||||||
syncAgentAppInstalls(appInstalls)
|
readOnlyMode := isDemoReadOnly(readOnly)
|
||||||
|
if !readOnlyMode {
|
||||||
|
syncAgentAppInstalls(appInstalls)
|
||||||
|
}
|
||||||
for index, item := range list {
|
for index, item := range list {
|
||||||
appInstall := appInstalls[index]
|
appInstall := appInstalls[index]
|
||||||
envMap := readInstallEnv(appInstall.Env)
|
envMap := readInstallEnv(appInstall.Env)
|
||||||
agentItem := buildAgentItem(&item, &appInstall, envMap)
|
agentItem := buildAgentItem(&item, &appInstall, envMap)
|
||||||
|
if readOnlyMode {
|
||||||
|
agentItem.Token = ""
|
||||||
|
agentItem.APIKey = ""
|
||||||
|
agentItem.DashboardPassword = ""
|
||||||
|
}
|
||||||
agentItem.Upgradable = checkAgentUpgradable(appInstall)
|
agentItem.Upgradable = checkAgentUpgradable(appInstall)
|
||||||
items = append(items, agentItem)
|
items = append(items, agentItem)
|
||||||
}
|
}
|
||||||
@@ -984,19 +1001,28 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
|
|||||||
}
|
}
|
||||||
apiTypes := make([]dto.ProviderAPIInfo, 0, len(def.APIConfigs))
|
apiTypes := make([]dto.ProviderAPIInfo, 0, len(def.APIConfigs))
|
||||||
for _, item := range def.APIConfigs {
|
for _, item := range def.APIConfigs {
|
||||||
|
apiModels := make([]dto.ProviderModelInfo, 0, len(item.Models))
|
||||||
|
for _, model := range item.Models {
|
||||||
|
apiModels = append(apiModels, dto.ProviderModelInfo{
|
||||||
|
ID: model.ID,
|
||||||
|
Name: model.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
apiTypes = append(apiTypes, dto.ProviderAPIInfo{
|
apiTypes = append(apiTypes, dto.ProviderAPIInfo{
|
||||||
APIType: item.APIType,
|
APIType: item.APIType,
|
||||||
BaseURL: item.BaseURL,
|
BaseURL: item.BaseURL,
|
||||||
EditableBaseURL: item.EditableBaseURL,
|
EditableBaseURL: item.EditableBaseURL,
|
||||||
DefaultAuthMode: item.DefaultAuthMode,
|
SupportsModelDiscovery: item.DiscoverModels,
|
||||||
AuthModes: item.AuthModes,
|
DefaultAuthMode: item.DefaultAuthMode,
|
||||||
|
AuthModes: item.AuthModes,
|
||||||
|
Models: apiModels,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
baseURL, _ := providercatalog.DefaultBaseURL(key)
|
baseURL, _ := providercatalog.DefaultBaseURL(key)
|
||||||
providers = append(providers, dto.ProviderInfo{
|
providers = append(providers, dto.ProviderInfo{
|
||||||
Sort: def.Sort,
|
Sort: def.Sort,
|
||||||
Provider: key,
|
Provider: key,
|
||||||
DisplayName: def.DisplayName,
|
DisplayName: localizedAgentProviderName(key),
|
||||||
BaseURL: baseURL,
|
BaseURL: baseURL,
|
||||||
DefaultAPIType: def.DefaultAPIType,
|
DefaultAPIType: def.DefaultAPIType,
|
||||||
APITypes: apiTypes,
|
APITypes: apiTypes,
|
||||||
@@ -1016,7 +1042,7 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
|||||||
if err := ensureAgentAccountNameAvailable(provider, req.Name, 0); err != nil {
|
if err := ensureAgentAccountNameAvailable(provider, req.Name, 0); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
initialModels, err := buildInitialAgentAccountModels(&model.AgentAccount{Provider: provider}, req.Models)
|
initialModels, err := buildInitialAgentAccountModels(&model.AgentAccount{Provider: provider, APIType: req.APIType}, req.Models)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1024,7 +1050,8 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel)
|
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
||||||
|
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1062,6 +1089,9 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if req.APIType != account.APIType {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "API type cannot be changed", nil)
|
||||||
|
}
|
||||||
provider := account.Provider
|
provider := account.Provider
|
||||||
if err := ensureAgentAccountNameAvailable(provider, req.Name, account.ID); err != nil {
|
if err := ensureAgentAccountNameAvailable(provider, req.Name, account.ID); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1078,7 +1108,8 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel)
|
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
||||||
|
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1105,11 +1136,17 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error) {
|
func (a AgentService) PageAccounts(req dto.AgentAccountSearch, readOnly ...bool) (int64, []dto.AgentAccountInfo, error) {
|
||||||
var opts []repo.DBOption
|
var opts []repo.DBOption
|
||||||
if strings.TrimSpace(req.Provider) != "" {
|
if strings.TrimSpace(req.Provider) != "" {
|
||||||
opts = append(opts, repo.WithByProvider(req.Provider))
|
opts = append(opts, repo.WithByProvider(req.Provider))
|
||||||
}
|
}
|
||||||
|
if apiType := strings.TrimSpace(req.APIType); apiType != "" {
|
||||||
|
opts = append(opts, repo.WithByAPIType(apiType))
|
||||||
|
}
|
||||||
|
if req.TextOnly {
|
||||||
|
opts = append(opts, repo.WithTextAPIType())
|
||||||
|
}
|
||||||
if strings.TrimSpace(req.Name) != "" {
|
if strings.TrimSpace(req.Name) != "" {
|
||||||
opts = append(opts, repo.WithByLikeName(req.Name))
|
opts = append(opts, repo.WithByLikeName(req.Name))
|
||||||
}
|
}
|
||||||
@@ -1120,14 +1157,14 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
|||||||
items := make([]dto.AgentAccountInfo, 0, len(list))
|
items := make([]dto.AgentAccountInfo, 0, len(list))
|
||||||
for _, item := range list {
|
for _, item := range list {
|
||||||
apiKey := ""
|
apiKey := ""
|
||||||
if item.RememberAPIKey {
|
if item.RememberAPIKey && !isDemoReadOnly(readOnly...) {
|
||||||
apiKey = item.APIKey
|
apiKey = item.APIKey
|
||||||
}
|
}
|
||||||
items = append(items, dto.AgentAccountInfo{
|
items = append(items, dto.AgentAccountInfo{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
MasterAccountID: item.MasterAccountID,
|
MasterAccountID: item.MasterAccountID,
|
||||||
Provider: item.Provider,
|
Provider: item.Provider,
|
||||||
ProviderName: providercatalog.DisplayName(item.Provider),
|
ProviderName: localizedAgentProviderName(item.Provider),
|
||||||
Name: item.Name,
|
Name: item.Name,
|
||||||
APIKey: apiKey,
|
APIKey: apiKey,
|
||||||
RememberAPIKey: item.RememberAPIKey,
|
RememberAPIKey: item.RememberAPIKey,
|
||||||
@@ -1166,7 +1203,7 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) {
|
func (a AgentService) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) {
|
||||||
return agentAccountRepo.CountByProviders(req.Providers)
|
return agentAccountRepo.CountTextByProviders(req.Providers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) {
|
func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) {
|
||||||
@@ -1178,7 +1215,8 @@ func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.Agen
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error) {
|
func (a AgentService) DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error) {
|
||||||
if req.APIType != "openai-completions" && req.APIType != "openai-responses" {
|
config, ok := providercatalog.FindAPIConfig(req.Provider, req.APIType)
|
||||||
|
if !ok || !config.DiscoverModels {
|
||||||
return nil, buserr.New("ErrAgentAccountModelsRequired")
|
return nil, buserr.New("ErrAgentAccountModelsRequired")
|
||||||
}
|
}
|
||||||
baseURL, err := providercatalog.ResolveBaseURL(req.Provider, req.APIType, req.BaseURL)
|
baseURL, err := providercatalog.ResolveBaseURL(req.Provider, req.APIType, req.BaseURL)
|
||||||
@@ -1320,7 +1358,7 @@ func (a AgentService) SyncAgentsByAccount(account *model.AgentAccount) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
|
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
|
||||||
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model)
|
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1410,7 +1448,7 @@ func (a AgentService) GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
auth := readHermesDashboardAuthFromInstall(install)
|
auth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
|
||||||
return &dto.AgentOtherConfig{
|
return &dto.AgentOtherConfig{
|
||||||
UserTimezone: cfg.Timezone,
|
UserTimezone: cfg.Timezone,
|
||||||
BrowserEnabled: true,
|
BrowserEnabled: true,
|
||||||
@@ -1419,6 +1457,13 @@ func (a AgentService) GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig,
|
|||||||
DashboardPassword: auth.Password,
|
DashboardPassword: auth.Password,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
if agent.AgentType == constant.AppCopaw {
|
||||||
|
auth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
|
||||||
|
return &dto.AgentOtherConfig{
|
||||||
|
DashboardUsername: auth.Username,
|
||||||
|
DashboardPassword: auth.Password,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1437,16 +1482,19 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if agent.AgentType == constant.AppHermesAgent {
|
if agent.AgentType == constant.AppHermesAgent {
|
||||||
|
if strings.TrimSpace(req.UserTimezone) == "" {
|
||||||
|
return buserr.New("ErrInvalidParams")
|
||||||
|
}
|
||||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID))
|
account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
previousAuth := readHermesDashboardAuthFromInstall(install)
|
previousAuth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
|
||||||
nextAuth := normalizeHermesDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
nextAuth := normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||||
if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil {
|
if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := writeHermesDashboardAuthEnv(path.Join(install.GetPath(), ".env"), nextAuth, true); err != nil {
|
if err := writeAgentDashboardAuthEnv(install.GetEnvPath(), agent.AgentType, nextAuth, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
operate := constant.Restart
|
operate := constant.Restart
|
||||||
@@ -1458,6 +1506,12 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
|
|||||||
Operate: operate,
|
Operate: operate,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if agent.AgentType == constant.AppCopaw {
|
||||||
|
return updateQwenPawDashboardAuth(install, normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword))
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.UserTimezone) == "" || strings.TrimSpace(req.NPMRegistry) == "" {
|
||||||
|
return buserr.New("ErrInvalidParams")
|
||||||
|
}
|
||||||
if err := ensureContainerRunning(install.ContainerName); err != nil {
|
if err := ensureContainerRunning(install.ContainerName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
)
|
||||||
|
|
||||||
|
type qwenPawAuthStatus struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
HasUsers bool `json:"has_users"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type qwenPawLoginResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateQwenPawDashboardAuth(install *model.AppInstall, next agentDashboardAuth) error {
|
||||||
|
if install == nil || install.ID == 0 {
|
||||||
|
return buserr.New("ErrRecordNotFound")
|
||||||
|
}
|
||||||
|
current, err := readAgentDashboardAuthEnv(install.GetEnvPath(), constant.AppCopaw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if current == next {
|
||||||
|
return writeAgentDashboardAuthEnv(install.GetEnvPath(), constant.AppCopaw, next, true)
|
||||||
|
}
|
||||||
|
if err := ensureContainerRunning(install.ContainerName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL := fmt.Sprintf("http://127.0.0.1:%d/api/auth", install.HttpPort)
|
||||||
|
var status qwenPawAuthStatus
|
||||||
|
if _, err := requestQwenPawAuth(http.MethodGet, baseURL+"/status", nil, "", &status); err != nil {
|
||||||
|
return buserr.WithMap("ErrQwenPawAuthRequest", map[string]interface{}{"err": err.Error()}, err)
|
||||||
|
}
|
||||||
|
if !status.Enabled {
|
||||||
|
return buserr.New("ErrQwenPawAuthDisabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !status.HasUsers {
|
||||||
|
payload := map[string]string{"username": next.Username, "password": next.Password}
|
||||||
|
if _, err := requestQwenPawAuth(http.MethodPost, baseURL+"/register", payload, "", nil); err != nil {
|
||||||
|
return buserr.WithMap("ErrQwenPawAuthRequest", map[string]interface{}{"err": err.Error()}, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var login qwenPawLoginResponse
|
||||||
|
payload := map[string]string{"username": current.Username, "password": current.Password}
|
||||||
|
statusCode, err := requestQwenPawAuth(http.MethodPost, baseURL+"/login", payload, "", &login)
|
||||||
|
if statusCode == http.StatusUnauthorized {
|
||||||
|
return buserr.New("ErrQwenPawAuthOutOfSync")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return buserr.WithMap("ErrQwenPawAuthRequest", map[string]interface{}{"err": err.Error()}, err)
|
||||||
|
}
|
||||||
|
payload = map[string]string{"current_password": current.Password}
|
||||||
|
if current.Username != next.Username {
|
||||||
|
payload["new_username"] = next.Username
|
||||||
|
}
|
||||||
|
if current.Password != next.Password {
|
||||||
|
payload["new_password"] = next.Password
|
||||||
|
}
|
||||||
|
if _, err := requestQwenPawAuth(http.MethodPost, baseURL+"/update-profile", payload, login.Token, nil); err != nil {
|
||||||
|
return buserr.WithMap("ErrQwenPawAuthRequest", map[string]interface{}{"err": err.Error()}, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return writeAgentDashboardAuthEnv(install.GetEnvPath(), constant.AppCopaw, next, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestQwenPawAuth(method, reqURL string, payload interface{}, token string, result interface{}) (int, error) {
|
||||||
|
var body io.Reader
|
||||||
|
if payload != nil {
|
||||||
|
data, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
body = bytes.NewReader(data)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, reqURL, body)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
if token != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
}
|
||||||
|
resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
data, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||||
|
if err != nil {
|
||||||
|
return resp.StatusCode, err
|
||||||
|
}
|
||||||
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||||
|
detail := strings.TrimSpace(string(data))
|
||||||
|
var errorResponse struct {
|
||||||
|
Detail string `json:"detail"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(data, &errorResponse) == nil && strings.TrimSpace(errorResponse.Detail) != "" {
|
||||||
|
detail = strings.TrimSpace(errorResponse.Detail)
|
||||||
|
}
|
||||||
|
if detail == "" {
|
||||||
|
detail = resp.Status
|
||||||
|
}
|
||||||
|
return resp.StatusCode, errors.New(detail)
|
||||||
|
}
|
||||||
|
if result != nil && len(data) > 0 {
|
||||||
|
if err := json.Unmarshal(data, result); err != nil {
|
||||||
|
return resp.StatusCode, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resp.StatusCode, nil
|
||||||
|
}
|
||||||
@@ -19,13 +19,6 @@ import (
|
|||||||
|
|
||||||
const hermesWorkspaceDir = "/opt/data/workspace"
|
const hermesWorkspaceDir = "/opt/data/workspace"
|
||||||
const hermesExecutablePath = "/opt/hermes/.venv/bin/hermes"
|
const hermesExecutablePath = "/opt/hermes/.venv/bin/hermes"
|
||||||
const hermesDashboardUsernameEnvKey = "HERMES_DASHBOARD_USERNAME"
|
|
||||||
const hermesDashboardPasswordEnvKey = "HERMES_DASHBOARD_PASSWORD"
|
|
||||||
|
|
||||||
type hermesDashboardAuth struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
type hermesConfig struct {
|
type hermesConfig struct {
|
||||||
Model hermesModelConfig `yaml:"model"`
|
Model hermesModelConfig `yaml:"model"`
|
||||||
@@ -117,52 +110,6 @@ func prepareHermesInstallFiles(appInstall *model.AppInstall, account *model.Agen
|
|||||||
return files.NewFileOp().ChownR(dataDir, "1000", "1000", true)
|
return files.NewFileOp().ChownR(dataDir, "1000", "1000", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeHermesDashboardAuth(username, password string) hermesDashboardAuth {
|
|
||||||
auth := hermesDashboardAuth{
|
|
||||||
Username: strings.TrimSpace(username),
|
|
||||||
Password: strings.TrimSpace(password),
|
|
||||||
}
|
|
||||||
if auth.Username == "" {
|
|
||||||
auth.Username = "admin"
|
|
||||||
}
|
|
||||||
if auth.Password == "" {
|
|
||||||
auth.Password = common.RandStr(8)
|
|
||||||
}
|
|
||||||
return auth
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeHermesDashboardAuthEnv(envPath string, auth hermesDashboardAuth, overwrite bool) error {
|
|
||||||
return upsertAgentEnv(envPath, map[string]string{
|
|
||||||
hermesDashboardUsernameEnvKey: auth.Username,
|
|
||||||
hermesDashboardPasswordEnvKey: auth.Password,
|
|
||||||
}, []string{
|
|
||||||
hermesDashboardUsernameEnvKey,
|
|
||||||
hermesDashboardPasswordEnvKey,
|
|
||||||
}, overwrite)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readHermesDashboardAuthEnv(envPath string) (hermesDashboardAuth, error) {
|
|
||||||
envMap, err := readAgentEnvMap(envPath)
|
|
||||||
if err != nil {
|
|
||||||
return hermesDashboardAuth{}, err
|
|
||||||
}
|
|
||||||
return hermesDashboardAuth{
|
|
||||||
Username: strings.TrimSpace(envMap[hermesDashboardUsernameEnvKey]),
|
|
||||||
Password: strings.TrimSpace(envMap[hermesDashboardPasswordEnvKey]),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readHermesDashboardAuthFromInstall(appInstall *model.AppInstall) hermesDashboardAuth {
|
|
||||||
if appInstall == nil || appInstall.ID == 0 {
|
|
||||||
return hermesDashboardAuth{}
|
|
||||||
}
|
|
||||||
auth, err := readHermesDashboardAuthEnv(path.Join(appInstall.GetPath(), ".env"))
|
|
||||||
if err != nil {
|
|
||||||
return hermesDashboardAuth{}
|
|
||||||
}
|
|
||||||
return auth
|
|
||||||
}
|
|
||||||
|
|
||||||
func readHermesConfig(configPath string) (*hermesConfig, error) {
|
func readHermesConfig(configPath string) (*hermesConfig, error) {
|
||||||
content, err := files.NewFileOp().GetContent(configPath)
|
content, err := files.NewFileOp().GetContent(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,315 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
openclawPluginPackagePattern = regexp.MustCompile(`^(@[a-z0-9][a-z0-9._-]*/)?[a-z0-9][a-z0-9._-]*$`)
|
||||||
|
openclawPluginVersionPattern = regexp.MustCompile(`^[0-9A-Za-z][0-9A-Za-z._-]*$`)
|
||||||
|
openclawPluginIDPattern = regexp.MustCompile(`^(@[A-Za-z0-9][A-Za-z0-9._-]*/)?[A-Za-z0-9][A-Za-z0-9._-]*$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type openclawPluginListOutput struct {
|
||||||
|
Plugins []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Origin string `json:"origin"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
} `json:"plugins"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type openclawPluginIndexItem struct {
|
||||||
|
PluginID string `json:"pluginId"`
|
||||||
|
PackageName string `json:"packageName"`
|
||||||
|
PackageVersion string `json:"packageVersion"`
|
||||||
|
Origin string `json:"origin"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type openclawPluginSearchOutput struct {
|
||||||
|
Results []struct {
|
||||||
|
Score float64 `json:"score"`
|
||||||
|
Package struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
RuntimeID string `json:"runtimeId"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
LatestVersion string `json:"latestVersion"`
|
||||||
|
Categories []string `json:"categories"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
IsOfficial bool `json:"isOfficial"`
|
||||||
|
VerificationTier string `json:"verificationTier"`
|
||||||
|
Stats struct {
|
||||||
|
Downloads int64 `json:"downloads"`
|
||||||
|
} `json:"stats"`
|
||||||
|
} `json:"package"`
|
||||||
|
} `json:"results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AgentService) ListPlugins(req dto.AgentPluginsReq) ([]dto.AgentPluginItem, error) {
|
||||||
|
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if plugins, err := readOpenclawPluginIndex(filepath.Join(filepath.Dir(agent.ConfigPath), "state", "openclaw.sqlite")); err == nil {
|
||||||
|
return plugins, nil
|
||||||
|
}
|
||||||
|
output, err := cmd.RunDockerExecWithStdout(2*time.Minute, install.ContainerName, "openclaw", "plugins", "list", "--json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return parseOpenclawPluginList([]byte(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AgentService) SearchPlugins(req dto.AgentPluginSearchReq) ([]dto.AgentPluginSearchItem, error) {
|
||||||
|
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
limit := req.Limit
|
||||||
|
if limit == 0 {
|
||||||
|
limit = 20
|
||||||
|
}
|
||||||
|
output, err := cmd.RunDockerExecWithStdout(
|
||||||
|
2*time.Minute,
|
||||||
|
install.ContainerName,
|
||||||
|
"openclaw", "plugins", "search", strings.TrimSpace(req.Keyword), "--limit", fmt.Sprint(limit), "--json",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return parseOpenclawPluginSearch([]byte(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AgentService) InstallMarketPlugin(req dto.AgentPluginMarketInstallReq) error {
|
||||||
|
spec, err := buildOpenclawPluginInstallSpec(req.Package, req.Version)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
taskName := fmt.Sprintf("%s [%s]", i18n.GetMsgByKey("AgentPluginInstall"), req.Package)
|
||||||
|
installTask, err := task.NewTask(taskName, task.TaskInstall, task.TaskScopeAI, req.TaskID, req.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
installTask.AddSubTask(taskName, func(t *task.Task) error {
|
||||||
|
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
|
||||||
|
return mgr.Run("docker", "exec", install.ContainerName, "openclaw", "plugins", "install", spec)
|
||||||
|
}, nil)
|
||||||
|
addOpenclawPluginRestartTask(installTask, install)
|
||||||
|
go executeAgentPluginTask(installTask)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AgentService) OperatePlugin(req dto.AgentPluginOperateReq) error {
|
||||||
|
if !openclawPluginIDPattern.MatchString(req.PluginID) {
|
||||||
|
return buserr.New("ErrInvalidChar")
|
||||||
|
}
|
||||||
|
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if req.Operate == "update" || req.Operate == "uninstall" {
|
||||||
|
plugins, err := a.ListPlugins(dto.AgentPluginsReq{AgentID: req.AgentID})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, plugin := range plugins {
|
||||||
|
if plugin.ID == req.PluginID && plugin.Origin == "bundled" {
|
||||||
|
return buserr.WithName("ErrNotSupportType", req.Operate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
taskType := map[string]string{
|
||||||
|
"enable": task.TaskUpdate,
|
||||||
|
"disable": task.TaskUpdate,
|
||||||
|
"update": task.TaskUpgrade,
|
||||||
|
"uninstall": task.TaskUninstall,
|
||||||
|
}[req.Operate]
|
||||||
|
taskName := fmt.Sprintf("%s [%s]", i18n.GetMsgByKey(map[string]string{
|
||||||
|
"enable": "AgentPluginEnable",
|
||||||
|
"disable": "AgentPluginDisable",
|
||||||
|
"update": "AgentPluginUpdate",
|
||||||
|
"uninstall": "AgentPluginUninstall",
|
||||||
|
}[req.Operate]), req.PluginID)
|
||||||
|
operateTask, err := task.NewTask(taskName, taskType, task.TaskScopeAI, req.TaskID, req.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
operateTask.AddSubTask(taskName, func(t *task.Task) error {
|
||||||
|
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
|
||||||
|
if req.Operate == "uninstall" {
|
||||||
|
if err := uninstallOpenclawPlugin(mgr, install.ContainerName, req.PluginID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return cleanupManagedOpenclawPlugin(agent, req.PluginID)
|
||||||
|
}
|
||||||
|
return mgr.Run("docker", "exec", install.ContainerName, "openclaw", "plugins", req.Operate, req.PluginID)
|
||||||
|
}, nil)
|
||||||
|
addOpenclawPluginRestartTask(operateTask, install)
|
||||||
|
go executeAgentPluginTask(operateTask)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOpenclawPluginList(raw []byte) ([]dto.AgentPluginItem, error) {
|
||||||
|
payload, err := extractEmbeddedJSON(string(raw))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(payload) == 0 {
|
||||||
|
return []dto.AgentPluginItem{}, nil
|
||||||
|
}
|
||||||
|
var output openclawPluginListOutput
|
||||||
|
if err := json.Unmarshal(payload, &output); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items := make([]dto.AgentPluginItem, 0, len(output.Plugins))
|
||||||
|
for _, plugin := range output.Plugins {
|
||||||
|
items = append(items, dto.AgentPluginItem{
|
||||||
|
ID: plugin.ID,
|
||||||
|
Name: plugin.Name,
|
||||||
|
Version: plugin.Version,
|
||||||
|
Origin: plugin.Origin,
|
||||||
|
Enabled: plugin.Enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readOpenclawPluginIndex(dbPath string) ([]dto.AgentPluginItem, error) {
|
||||||
|
db, err := sql.Open("sqlite", "file:"+filepath.ToSlash(dbPath)+"?mode=ro")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
var raw []byte
|
||||||
|
if err := db.QueryRow(
|
||||||
|
"SELECT plugins_json FROM installed_plugin_index WHERE index_key = ?",
|
||||||
|
"installed-plugin-index",
|
||||||
|
).Scan(&raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var plugins []openclawPluginIndexItem
|
||||||
|
if err := json.Unmarshal(raw, &plugins); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items := make([]dto.AgentPluginItem, 0, len(plugins))
|
||||||
|
for _, plugin := range plugins {
|
||||||
|
name := plugin.PackageName
|
||||||
|
if name == "" {
|
||||||
|
name = plugin.PluginID
|
||||||
|
}
|
||||||
|
items = append(items, dto.AgentPluginItem{
|
||||||
|
ID: plugin.PluginID,
|
||||||
|
Name: name,
|
||||||
|
Version: plugin.PackageVersion,
|
||||||
|
Origin: plugin.Origin,
|
||||||
|
Enabled: plugin.Enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOpenclawPluginSearch(raw []byte) ([]dto.AgentPluginSearchItem, error) {
|
||||||
|
payload, err := extractEmbeddedJSON(string(raw))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(payload) == 0 {
|
||||||
|
return []dto.AgentPluginSearchItem{}, nil
|
||||||
|
}
|
||||||
|
var output openclawPluginSearchOutput
|
||||||
|
if err := json.Unmarshal(payload, &output); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items := make([]dto.AgentPluginSearchItem, 0, len(output.Results))
|
||||||
|
for _, result := range output.Results {
|
||||||
|
items = append(items, dto.AgentPluginSearchItem{
|
||||||
|
Package: result.Package.Name,
|
||||||
|
PluginID: result.Package.RuntimeID,
|
||||||
|
Name: result.Package.DisplayName,
|
||||||
|
Description: result.Package.Summary,
|
||||||
|
Version: result.Package.LatestVersion,
|
||||||
|
Channel: result.Package.Channel,
|
||||||
|
VerificationTier: result.Package.VerificationTier,
|
||||||
|
Categories: append([]string{}, result.Package.Categories...),
|
||||||
|
Official: result.Package.IsOfficial,
|
||||||
|
Downloads: result.Package.Stats.Downloads,
|
||||||
|
Score: result.Score,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildOpenclawPluginInstallSpec(packageName, version string) (string, error) {
|
||||||
|
packageName = strings.TrimSpace(packageName)
|
||||||
|
version = strings.TrimSpace(version)
|
||||||
|
if !openclawPluginPackagePattern.MatchString(packageName) || !openclawPluginVersionPattern.MatchString(version) {
|
||||||
|
return "", buserr.New("ErrInvalidChar")
|
||||||
|
}
|
||||||
|
return "clawhub:" + packageName + "@" + version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupManagedOpenclawPlugin(agent *model.Agent, pluginID string) error {
|
||||||
|
pluginType := map[string]string{
|
||||||
|
"openclaw-lark": "feishu",
|
||||||
|
"openclaw-qqbot": "qqbot",
|
||||||
|
"wecom-openclaw-plugin": "wecom",
|
||||||
|
"dingtalk-connector": "dingtalk",
|
||||||
|
"openclaw-weixin": "weixin",
|
||||||
|
}[pluginID]
|
||||||
|
if pluginType == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cleanupOpenclawPluginConfig(conf, pluginType)
|
||||||
|
return writeOpenclawConfigRaw(agent.ConfigPath, conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addOpenclawPluginRestartTask(t *task.Task, install *model.AppInstall) {
|
||||||
|
t.AddSubTask(task.GetTaskName("OpenClaw", task.TaskRestart, task.TaskScopeAI), func(t *task.Task) error {
|
||||||
|
output, err := compose.Restart(install.GetComposePath())
|
||||||
|
if output != "" {
|
||||||
|
t.Log(output)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeAgentPluginTask(t *task.Task) {
|
||||||
|
if err := t.Execute(); err != nil {
|
||||||
|
global.LOG.Errorf("operate openclaw plugin failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
|
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||||
@@ -96,7 +97,7 @@ func ensureContainerRunning(containerName string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string) (resolvedAgentAccountInput, error) {
|
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string, validateAvailability bool) (resolvedAgentAccountInput, error) {
|
||||||
resolvedAPIKey := strings.TrimSpace(apiKey)
|
resolvedAPIKey := strings.TrimSpace(apiKey)
|
||||||
resolvedAPIType := strings.TrimSpace(apiType)
|
resolvedAPIType := strings.TrimSpace(apiType)
|
||||||
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode)
|
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode)
|
||||||
@@ -114,7 +115,8 @@ func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, mode
|
|||||||
if modelID == "" {
|
if modelID == "" {
|
||||||
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
|
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
|
||||||
}
|
}
|
||||||
if !providercatalog.SkipVerification(provider) {
|
imageAPI := providercatalog.IsImageAPIType(resolvedAPIType)
|
||||||
|
if validateAvailability && (imageAPI || providercatalog.IsEmbeddingAPIType(resolvedAPIType) || !providercatalog.SkipVerification(provider)) {
|
||||||
if err := providercatalog.VerifyAccount(provider, resolvedAPIType, resolvedAuthMode, resolvedBaseURL, resolvedAPIKey, modelID); err != nil {
|
if err := providercatalog.VerifyAccount(provider, resolvedAPIType, resolvedAuthMode, resolvedBaseURL, resolvedAPIKey, modelID); err != nil {
|
||||||
return resolvedAgentAccountInput{}, err
|
return resolvedAgentAccountInput{}, err
|
||||||
}
|
}
|
||||||
@@ -386,7 +388,7 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
|||||||
Remark: agent.Remark,
|
Remark: agent.Remark,
|
||||||
AgentType: agentType,
|
AgentType: agentType,
|
||||||
Provider: agent.Provider,
|
Provider: agent.Provider,
|
||||||
ProviderName: providercatalog.DisplayName(agent.Provider),
|
ProviderName: localizedAgentProviderName(agent.Provider),
|
||||||
Model: agent.Model,
|
Model: agent.Model,
|
||||||
APIType: agent.APIType,
|
APIType: agent.APIType,
|
||||||
BaseURL: agent.BaseURL,
|
BaseURL: agent.BaseURL,
|
||||||
@@ -420,8 +422,8 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
|||||||
item.BridgePort = toInt(bridge)
|
item.BridgePort = toInt(bridge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if agentType == constant.AppHermesAgent {
|
if _, _, ok := agentDashboardAuthEnvKeys(agentType); ok {
|
||||||
auth := readHermesDashboardAuthFromInstall(appInstall)
|
auth := readAgentDashboardAuthFromInstall(appInstall, agentType)
|
||||||
item.DashboardUsername = auth.Username
|
item.DashboardUsername = auth.Username
|
||||||
item.DashboardPassword = auth.Password
|
item.DashboardPassword = auth.Password
|
||||||
}
|
}
|
||||||
@@ -429,6 +431,15 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
|||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func localizedAgentProviderName(provider string) string {
|
||||||
|
if key := providercatalog.DisplayNameKey(provider); key != "" {
|
||||||
|
if name := strings.TrimSpace(i18n.GetMsgByKey(key)); name != "" {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return providercatalog.DisplayName(provider)
|
||||||
|
}
|
||||||
|
|
||||||
func isAgentAppKey(appKey string) bool {
|
func isAgentAppKey(appKey string) bool {
|
||||||
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
|
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
|
||||||
}
|
}
|
||||||
@@ -1159,15 +1170,15 @@ func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto
|
|||||||
if len(requested) > 0 {
|
if len(requested) > 0 {
|
||||||
return normalizeAgentAccountModels(account, requested)
|
return normalizeAgentAccountModels(account, requested)
|
||||||
}
|
}
|
||||||
meta, ok := providercatalog.Get(account.Provider)
|
defaultModels := providercatalog.DefaultModels(account.Provider, account.APIType)
|
||||||
if !ok || len(meta.Models) == 0 {
|
if len(defaultModels) == 0 {
|
||||||
if requiresInitialAgentAccountModels(account.Provider) {
|
if requiresInitialAgentAccountModels(account.Provider) {
|
||||||
return nil, buserr.New("ErrAgentAccountModelsRequired")
|
return nil, buserr.New("ErrAgentAccountModelsRequired")
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
requested = make([]dto.AgentAccountModel, 0, len(meta.Models))
|
requested = make([]dto.AgentAccountModel, 0, len(defaultModels))
|
||||||
for _, item := range meta.Models {
|
for _, item := range defaultModels {
|
||||||
requested = append(requested, dto.AgentAccountModel{
|
requested = append(requested, dto.AgentAccountModel{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
Name: item.Name,
|
Name: item.Name,
|
||||||
@@ -1452,6 +1463,87 @@ func readInstallEnv(envStr string) map[string]interface{} {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
hermesDashboardUsernameEnvKey = "HERMES_DASHBOARD_USERNAME"
|
||||||
|
hermesDashboardPasswordEnvKey = "HERMES_DASHBOARD_PASSWORD"
|
||||||
|
qwenPawAuthEnabledEnvKey = "QWENPAW_AUTH_ENABLED"
|
||||||
|
qwenPawAuthUsernameEnvKey = "QWENPAW_AUTH_USERNAME"
|
||||||
|
qwenPawAuthPasswordEnvKey = "QWENPAW_AUTH_PASSWORD"
|
||||||
|
)
|
||||||
|
|
||||||
|
type agentDashboardAuth struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeAgentDashboardAuth(username, password string) agentDashboardAuth {
|
||||||
|
auth := agentDashboardAuth{
|
||||||
|
Username: strings.TrimSpace(username),
|
||||||
|
Password: strings.TrimSpace(password),
|
||||||
|
}
|
||||||
|
if auth.Username == "" {
|
||||||
|
auth.Username = "admin"
|
||||||
|
}
|
||||||
|
if auth.Password == "" {
|
||||||
|
auth.Password = common.RandStr(8)
|
||||||
|
}
|
||||||
|
return auth
|
||||||
|
}
|
||||||
|
|
||||||
|
func agentDashboardAuthEnvKeys(agentType string) (string, string, bool) {
|
||||||
|
switch agentType {
|
||||||
|
case constant.AppHermesAgent:
|
||||||
|
return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true
|
||||||
|
case constant.AppCopaw:
|
||||||
|
return qwenPawAuthUsernameEnvKey, qwenPawAuthPasswordEnvKey, true
|
||||||
|
default:
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeAgentDashboardAuthEnv(envPath, agentType string, auth agentDashboardAuth, overwrite bool) error {
|
||||||
|
usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("dashboard auth is not supported for %s", agentType)
|
||||||
|
}
|
||||||
|
values := map[string]string{
|
||||||
|
usernameKey: auth.Username,
|
||||||
|
passwordKey: auth.Password,
|
||||||
|
}
|
||||||
|
order := []string{usernameKey, passwordKey}
|
||||||
|
if agentType == constant.AppCopaw {
|
||||||
|
values[qwenPawAuthEnabledEnvKey] = "true"
|
||||||
|
order = append([]string{qwenPawAuthEnabledEnvKey}, order...)
|
||||||
|
}
|
||||||
|
return upsertAgentEnv(envPath, values, order, overwrite)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAgentDashboardAuthEnv(envPath, agentType string) (agentDashboardAuth, error) {
|
||||||
|
usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType)
|
||||||
|
if !ok {
|
||||||
|
return agentDashboardAuth{}, fmt.Errorf("dashboard auth is not supported for %s", agentType)
|
||||||
|
}
|
||||||
|
envMap, err := readAgentEnvMap(envPath)
|
||||||
|
if err != nil {
|
||||||
|
return agentDashboardAuth{}, err
|
||||||
|
}
|
||||||
|
return agentDashboardAuth{
|
||||||
|
Username: strings.TrimSpace(envMap[usernameKey]),
|
||||||
|
Password: strings.TrimSpace(envMap[passwordKey]),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAgentDashboardAuthFromInstall(appInstall *model.AppInstall, agentType string) agentDashboardAuth {
|
||||||
|
if appInstall == nil || appInstall.ID == 0 {
|
||||||
|
return agentDashboardAuth{}
|
||||||
|
}
|
||||||
|
auth, err := readAgentDashboardAuthEnv(appInstall.GetEnvPath(), agentType)
|
||||||
|
if err != nil {
|
||||||
|
return agentDashboardAuth{}
|
||||||
|
}
|
||||||
|
return auth
|
||||||
|
}
|
||||||
|
|
||||||
func readAgentEnvMap(envPath string) (map[string]string, error) {
|
func readAgentEnvMap(envPath string) (map[string]string, error) {
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
if !fileOp.Stat(envPath) {
|
if !fileOp.Stat(envPath) {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ type IAlertService interface {
|
|||||||
GetCronJobs(req dto.CronJobReq) ([]dto.CronJobDTO, error)
|
GetCronJobs(req dto.CronJobReq) ([]dto.CronJobDTO, error)
|
||||||
|
|
||||||
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
|
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
|
||||||
PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error)
|
PageAlertConfig(req dto.AlertConfigPageReq, readOnly ...bool) (int64, []model.AlertConfig, error)
|
||||||
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
|
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
|
||||||
DeleteAlertConfig(id uint) error
|
DeleteAlertConfig(id uint) error
|
||||||
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
|
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
|
||||||
@@ -384,6 +384,9 @@ func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.Ale
|
|||||||
if search.Count != 0 {
|
if search.Count != 0 {
|
||||||
opts = append(opts, alertRepo.WithByCount(search.Count))
|
opts = append(opts, alertRepo.WithByCount(search.Count))
|
||||||
}
|
}
|
||||||
|
if !search.StartTime.IsZero() && !search.EndTime.IsZero() {
|
||||||
|
opts = append(opts, repo.WithByCreatedAt(search.StartTime, search.EndTime))
|
||||||
|
}
|
||||||
opts = append(opts, repo.WithOrderDesc("created_at"))
|
opts = append(opts, repo.WithOrderDesc("created_at"))
|
||||||
|
|
||||||
total, alerts, err := alertRepo.PageLog(search.Page, search.PageSize, opts...)
|
total, alerts, err := alertRepo.PageLog(search.Page, search.PageSize, opts...)
|
||||||
@@ -494,7 +497,7 @@ func (a AlertService) GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertCon
|
|||||||
return configs, err
|
return configs, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error) {
|
func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq, readOnly ...bool) (int64, []model.AlertConfig, error) {
|
||||||
opts := []repo.DBOption{
|
opts := []repo.DBOption{
|
||||||
alertRepo.WithByTypeNotIn([]string{"common"}),
|
alertRepo.WithByTypeNotIn([]string{"common"}),
|
||||||
repo.WithOrderDesc("created_at"),
|
repo.WithOrderDesc("created_at"),
|
||||||
@@ -502,7 +505,25 @@ func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []mode
|
|||||||
if len(req.ExcludeTypes) > 0 {
|
if len(req.ExcludeTypes) > 0 {
|
||||||
opts = append(opts, alertRepo.WithByTypeNotIn(req.ExcludeTypes))
|
opts = append(opts, alertRepo.WithByTypeNotIn(req.ExcludeTypes))
|
||||||
}
|
}
|
||||||
return alertRepo.PageAlertConfig(req.Page, req.PageSize, opts...)
|
total, configs, err := alertRepo.PageAlertConfig(req.Page, req.PageSize, opts...)
|
||||||
|
if err != nil || !isDemoReadOnly(readOnly...) {
|
||||||
|
return total, configs, err
|
||||||
|
}
|
||||||
|
for i := range configs {
|
||||||
|
var value interface{}
|
||||||
|
if err := json.Unmarshal([]byte(configs[i].Config), &value); err != nil {
|
||||||
|
configs[i].Config = ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
redactSensitiveData(value)
|
||||||
|
data, err := json.Marshal(value)
|
||||||
|
if err != nil {
|
||||||
|
configs[i].Config = ""
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
configs[i].Config = string(data)
|
||||||
|
}
|
||||||
|
return total, configs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error {
|
func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error {
|
||||||
|
|||||||
+40
-35
@@ -48,7 +48,7 @@ type IAppService interface {
|
|||||||
PageApp(ctx *gin.Context, req request.AppSearch) (*response.AppRes, error)
|
PageApp(ctx *gin.Context, req request.AppSearch) (*response.AppRes, error)
|
||||||
GetAppTags(ctx *gin.Context) ([]response.TagDTO, error)
|
GetAppTags(ctx *gin.Context) ([]response.TagDTO, error)
|
||||||
GetApp(ctx *gin.Context, key string) (*response.AppDTO, error)
|
GetApp(ctx *gin.Context, key string) (*response.AppDTO, error)
|
||||||
GetAppDetail(appId uint, version, appType string) (response.AppDetailDTO, error)
|
GetAppDetail(appId uint, version, appType string, readOnly ...bool) (response.AppDetailDTO, error)
|
||||||
Install(req request.AppInstallCreate, executeScript bool) (*model.AppInstall, error)
|
Install(req request.AppInstallCreate, executeScript bool) (*model.AppInstall, error)
|
||||||
SyncAppListFromRemote(taskID string) error
|
SyncAppListFromRemote(taskID string) error
|
||||||
GetAppUpdate() (*response.AppUpdateRes, error)
|
GetAppUpdate() (*response.AppUpdateRes, error)
|
||||||
@@ -231,7 +231,7 @@ func (a AppService) GetAppDetailByKey(appKey, version string) (response.AppDetai
|
|||||||
return appDetailDTO, nil
|
return appDetailDTO, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AppService) GetAppDetail(appID uint, version, appType string) (response.AppDetailDTO, error) {
|
func (a AppService) GetAppDetail(appID uint, version, appType string, readOnly ...bool) (response.AppDetailDTO, error) {
|
||||||
var (
|
var (
|
||||||
appDetailDTO response.AppDetailDTO
|
appDetailDTO response.AppDetailDTO
|
||||||
opts []repo.DBOption
|
opts []repo.DBOption
|
||||||
@@ -243,6 +243,7 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
|||||||
}
|
}
|
||||||
appDetailDTO.AppDetail = detail
|
appDetailDTO.AppDetail = detail
|
||||||
appDetailDTO.Enable = true
|
appDetailDTO.Enable = true
|
||||||
|
readOnlyMode := isDemoReadOnly(readOnly...)
|
||||||
|
|
||||||
if appType == "runtime" {
|
if appType == "runtime" {
|
||||||
app, err := appRepo.GetFirst(repo.WithByID(appID))
|
app, err := appRepo.GetFirst(repo.WithByID(appID))
|
||||||
@@ -252,42 +253,46 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
|||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
|
|
||||||
versionPath := filepath.Join(app.GetAppResourcePath(), detail.Version)
|
versionPath := filepath.Join(app.GetAppResourcePath(), detail.Version)
|
||||||
if !fileOp.Stat(versionPath) || detail.Update {
|
versionExists := fileOp.Stat(versionPath)
|
||||||
|
if (!versionExists || detail.Update) && !readOnlyMode {
|
||||||
if err = downloadApp(app, detail, nil, nil); err != nil && !fileOp.Stat(versionPath) {
|
if err = downloadApp(app, detail, nil, nil); err != nil && !fileOp.Stat(versionPath) {
|
||||||
return appDetailDTO, err
|
return appDetailDTO, err
|
||||||
}
|
}
|
||||||
|
versionExists = fileOp.Stat(versionPath)
|
||||||
}
|
}
|
||||||
switch app.Type {
|
if versionExists {
|
||||||
case constant.RuntimePHP:
|
switch app.Type {
|
||||||
paramsPath := filepath.Join(versionPath, "data.yml")
|
case constant.RuntimePHP:
|
||||||
if !fileOp.Stat(paramsPath) {
|
paramsPath := filepath.Join(versionPath, "data.yml")
|
||||||
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", paramsPath, nil)
|
if !fileOp.Stat(paramsPath) {
|
||||||
}
|
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", paramsPath, nil)
|
||||||
param, err := fileOp.GetContent(paramsPath)
|
}
|
||||||
if err != nil {
|
param, err := fileOp.GetContent(paramsPath)
|
||||||
return appDetailDTO, err
|
if err != nil {
|
||||||
}
|
return appDetailDTO, err
|
||||||
paramMap := make(map[string]interface{})
|
}
|
||||||
if err = yaml.Unmarshal(param, ¶mMap); err != nil {
|
paramMap := make(map[string]interface{})
|
||||||
return appDetailDTO, err
|
if err = yaml.Unmarshal(param, ¶mMap); err != nil {
|
||||||
}
|
return appDetailDTO, err
|
||||||
appDetailDTO.Params = paramMap["additionalProperties"]
|
}
|
||||||
composePath := filepath.Join(versionPath, "docker-compose.yml")
|
appDetailDTO.Params = paramMap["additionalProperties"]
|
||||||
if !fileOp.Stat(composePath) {
|
composePath := filepath.Join(versionPath, "docker-compose.yml")
|
||||||
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", composePath, nil)
|
if !fileOp.Stat(composePath) {
|
||||||
}
|
return appDetailDTO, buserr.WithDetail("ErrFileNotExist", composePath, nil)
|
||||||
compose, err := fileOp.GetContent(composePath)
|
}
|
||||||
if err != nil {
|
compose, err := fileOp.GetContent(composePath)
|
||||||
return appDetailDTO, err
|
if err != nil {
|
||||||
}
|
return appDetailDTO, err
|
||||||
composeMap := make(map[string]interface{})
|
}
|
||||||
if err := yaml.Unmarshal(compose, &composeMap); err != nil {
|
composeMap := make(map[string]interface{})
|
||||||
return appDetailDTO, err
|
if err := yaml.Unmarshal(compose, &composeMap); err != nil {
|
||||||
}
|
return appDetailDTO, err
|
||||||
if service, ok := composeMap["services"]; ok {
|
}
|
||||||
servicesMap := service.(map[string]interface{})
|
if service, ok := composeMap["services"]; ok {
|
||||||
for k := range servicesMap {
|
servicesMap := service.(map[string]interface{})
|
||||||
appDetailDTO.Image = k
|
for k := range servicesMap {
|
||||||
|
appDetailDTO.Image = k
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -299,7 +304,7 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
|
|||||||
appDetailDTO.Params = paramMap
|
appDetailDTO.Params = paramMap
|
||||||
}
|
}
|
||||||
|
|
||||||
if appDetailDTO.DockerCompose == "" {
|
if appDetailDTO.DockerCompose == "" && !readOnlyMode {
|
||||||
filename := filepath.Base(appDetailDTO.DownloadUrl)
|
filename := filepath.Base(appDetailDTO.DownloadUrl)
|
||||||
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml")
|
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml")
|
||||||
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type AppIgnoreUpgradeService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type IAppIgnoreUpgradeService interface {
|
type IAppIgnoreUpgradeService interface {
|
||||||
List() ([]response.AppIgnoreUpgradeDTO, error)
|
List(readOnly ...bool) ([]response.AppIgnoreUpgradeDTO, error)
|
||||||
CreateAppIgnore(req request.AppIgnoreUpgradeReq) error
|
CreateAppIgnore(req request.AppIgnoreUpgradeReq) error
|
||||||
Delete(req request.ReqWithID) error
|
Delete(req request.ReqWithID) error
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ func NewIAppIgnoreUpgradeService() IAppIgnoreUpgradeService {
|
|||||||
return AppIgnoreUpgradeService{}
|
return AppIgnoreUpgradeService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AppIgnoreUpgradeService) List() ([]response.AppIgnoreUpgradeDTO, error) {
|
func (a AppIgnoreUpgradeService) List(readOnly ...bool) ([]response.AppIgnoreUpgradeDTO, error) {
|
||||||
var res []response.AppIgnoreUpgradeDTO
|
var res []response.AppIgnoreUpgradeDTO
|
||||||
ignores, err := appIgnoreUpgradeRepo.List()
|
ignores, err := appIgnoreUpgradeRepo.List()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -37,14 +37,18 @@ func (a AppIgnoreUpgradeService) List() ([]response.AppIgnoreUpgradeDTO, error)
|
|||||||
}
|
}
|
||||||
app, err := appRepo.GetFirst(repo.WithByID(ignore.AppID))
|
app, err := appRepo.GetFirst(repo.WithByID(ignore.AppID))
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
_ = appIgnoreUpgradeRepo.Delete(repo.WithByID(ignore.ID))
|
if !isDemoReadOnly(readOnly...) {
|
||||||
|
_ = appIgnoreUpgradeRepo.Delete(repo.WithByID(ignore.ID))
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dto.Name = app.Name
|
dto.Name = app.Name
|
||||||
if ignore.Scope == "version" {
|
if ignore.Scope == "version" {
|
||||||
appDetail, err := appDetailRepo.GetFirst(repo.WithByID(ignore.AppDetailID))
|
appDetail, err := appDetailRepo.GetFirst(repo.WithByID(ignore.AppDetailID))
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
_ = appIgnoreUpgradeRepo.Delete(repo.WithByID(ignore.ID))
|
if !isDemoReadOnly(readOnly...) {
|
||||||
|
_ = appIgnoreUpgradeRepo.Delete(repo.WithByID(ignore.ID))
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dto.Version = appDetail.Version
|
dto.Version = appDetail.Version
|
||||||
|
|||||||
@@ -42,21 +42,21 @@ type IAppInstallService interface {
|
|||||||
Page(req request.AppInstalledSearch) (int64, []response.AppInstallDTO, error)
|
Page(req request.AppInstalledSearch) (int64, []response.AppInstallDTO, error)
|
||||||
CheckExist(req request.AppInstalledInfo) (*response.AppInstalledCheck, error)
|
CheckExist(req request.AppInstalledInfo) (*response.AppInstalledCheck, error)
|
||||||
LoadPort(req dto.OperationWithNameAndType) (int64, error)
|
LoadPort(req dto.OperationWithNameAndType) (int64, error)
|
||||||
LoadConnInfo(req dto.OperationWithNameAndType) (response.DatabaseConn, error)
|
LoadConnInfo(req dto.OperationWithNameAndType, readOnly ...bool) (response.DatabaseConn, error)
|
||||||
SearchForWebsite(req request.AppInstalledSearch) ([]response.AppInstallDTO, error)
|
SearchForWebsite(req request.AppInstalledSearch) ([]response.AppInstallDTO, error)
|
||||||
Operate(req request.AppInstalledOperate) error
|
Operate(req request.AppInstalledOperate) error
|
||||||
Update(req request.AppInstalledUpdate) error
|
Update(req request.AppInstalledUpdate) error
|
||||||
SyncAll(systemInit bool) error
|
SyncAll(systemInit bool) error
|
||||||
GetServices(key string) ([]response.AppService, error)
|
GetServices(key string) ([]response.AppService, error)
|
||||||
GetUpdateVersions(req request.AppUpdateVersion) ([]dto.AppVersion, error)
|
GetUpdateVersions(req request.AppUpdateVersion) ([]dto.AppVersion, error)
|
||||||
GetParams(id uint) (*response.AppConfig, error)
|
GetParams(id uint, readOnly ...bool) (*response.AppConfig, error)
|
||||||
ChangeAppPort(req request.PortUpdate) error
|
ChangeAppPort(req request.PortUpdate) error
|
||||||
GetDefaultConfigByKey(key, name string) (string, error)
|
GetDefaultConfigByKey(key, name string) (string, error)
|
||||||
DeleteCheck(installId uint) ([]dto.AppResource, error)
|
DeleteCheck(installId uint, readOnly ...bool) ([]dto.AppResource, error)
|
||||||
|
|
||||||
UpdateAppConfig(req request.AppConfigUpdate) error
|
UpdateAppConfig(req request.AppConfigUpdate) error
|
||||||
GetInstallList() ([]dto.AppInstallInfo, error)
|
GetInstallList() ([]dto.AppInstallInfo, error)
|
||||||
GetAppInstallInfo(appInstallID uint) (*response.AppInstallInfo, error)
|
GetAppInstallInfo(appInstallID uint, readOnly ...bool) (*response.AppInstallInfo, error)
|
||||||
UpdateSort(req request.AppInstallSort) error
|
UpdateSort(req request.AppInstallSort) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ func (a *AppInstallService) Page(req request.AppInstalledSearch) (int64, []respo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
installDTOs, _ := handleInstalled(installs, req.Update, req.Sync, req.CheckUpdate)
|
installDTOs, _ := handleInstalled(installs, req.Update, req.Sync && !req.ReadOnly, req.CheckUpdate, req.ReadOnly)
|
||||||
if req.Update {
|
if req.Update {
|
||||||
total = int64(len(installDTOs))
|
total = int64(len(installDTOs))
|
||||||
}
|
}
|
||||||
@@ -151,8 +151,10 @@ func (a *AppInstallService) CheckExist(req request.AppInstalledInfo) (*response.
|
|||||||
if reflect.DeepEqual(appInstall, model.AppInstall{}) {
|
if reflect.DeepEqual(appInstall, model.AppInstall{}) {
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
if err = syncAppInstallStatus(&appInstall, false); err != nil {
|
if !req.ReadOnly {
|
||||||
return nil, err
|
if err = syncAppInstallStatus(&appInstall, false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.ContainerName = appInstall.ContainerName
|
res.ContainerName = appInstall.ContainerName
|
||||||
@@ -182,7 +184,7 @@ func (a *AppInstallService) LoadPort(req dto.OperationWithNameAndType) (int64, e
|
|||||||
return app.Port, nil
|
return app.Port, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AppInstallService) LoadConnInfo(req dto.OperationWithNameAndType) (response.DatabaseConn, error) {
|
func (a *AppInstallService) LoadConnInfo(req dto.OperationWithNameAndType, readOnly ...bool) (response.DatabaseConn, error) {
|
||||||
var data response.DatabaseConn
|
var data response.DatabaseConn
|
||||||
app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name)
|
app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -191,6 +193,9 @@ func (a *AppInstallService) LoadConnInfo(req dto.OperationWithNameAndType) (resp
|
|||||||
data.Status = app.Status
|
data.Status = app.Status
|
||||||
data.Username = app.UserName
|
data.Username = app.UserName
|
||||||
data.Password = app.Password
|
data.Password = app.Password
|
||||||
|
if isDemoReadOnly(readOnly...) {
|
||||||
|
data.Password = ""
|
||||||
|
}
|
||||||
data.ServiceName = app.ServiceName
|
data.ServiceName = app.ServiceName
|
||||||
data.Port = app.Port
|
data.Port = app.Port
|
||||||
data.ContainerName = app.ContainerName
|
data.ContainerName = app.ContainerName
|
||||||
@@ -240,7 +245,7 @@ func (a *AppInstallService) SearchForWebsite(req request.AppInstalledSearch) ([]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return handleInstalled(installs, false, true, false)
|
return handleInstalled(installs, false, !req.ReadOnly, false, req.ReadOnly)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
|
func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
|
||||||
@@ -664,7 +669,7 @@ func (a *AppInstallService) ChangeAppPort(req request.PortUpdate) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AppInstallService) DeleteCheck(installID uint) ([]dto.AppResource, error) {
|
func (a *AppInstallService) DeleteCheck(installID uint, readOnly ...bool) ([]dto.AppResource, error) {
|
||||||
var res []dto.AppResource
|
var res []dto.AppResource
|
||||||
appInstall, err := appInstallRepo.GetFirst(repo.WithByID(installID))
|
appInstall, err := appInstallRepo.GetFirst(repo.WithByID(installID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -685,7 +690,7 @@ func (a *AppInstallService) DeleteCheck(installID uint) ([]dto.AppResource, erro
|
|||||||
Type: "app",
|
Type: "app",
|
||||||
Name: linkInstall.Name,
|
Name: linkInstall.Name,
|
||||||
})
|
})
|
||||||
} else {
|
} else if !isDemoReadOnly(readOnly...) {
|
||||||
_ = appInstallResourceRepo.DeleteBy(context.Background(), appInstallResourceRepo.WithAppInstallId(resource.AppInstallId))
|
_ = appInstallResourceRepo.DeleteBy(context.Background(), appInstallResourceRepo.WithAppInstallId(resource.AppInstallId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -723,7 +728,7 @@ func (a *AppInstallService) GetDefaultConfigByKey(key, name string) (string, err
|
|||||||
return string(contentByte), nil
|
return string(contentByte), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
|
func (a *AppInstallService) GetParams(id uint, readOnly ...bool) (*response.AppConfig, error) {
|
||||||
var (
|
var (
|
||||||
params []response.AppParam
|
params []response.AppParam
|
||||||
appForm dto.AppForm
|
appForm dto.AppForm
|
||||||
@@ -818,8 +823,14 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readOnlyMode := isDemoReadOnly(readOnly...)
|
||||||
|
if readOnlyMode {
|
||||||
|
redactSensitiveAppParams(params)
|
||||||
|
}
|
||||||
config := getAppCommonConfig(envs)
|
config := getAppCommonConfig(envs)
|
||||||
config.DockerCompose = install.DockerCompose
|
if !readOnlyMode {
|
||||||
|
config.DockerCompose = install.DockerCompose
|
||||||
|
}
|
||||||
res.Params = params
|
res.Params = params
|
||||||
if config.ContainerName == "" {
|
if config.ContainerName == "" {
|
||||||
config.ContainerName = install.ContainerName
|
config.ContainerName = install.ContainerName
|
||||||
@@ -829,8 +840,10 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
|
|||||||
res.RestartPolicy = getRestartPolicy(install.DockerCompose)
|
res.RestartPolicy = getRestartPolicy(install.DockerCompose)
|
||||||
res.WebUI = install.WebUI
|
res.WebUI = install.WebUI
|
||||||
res.Type = install.App.Type
|
res.Type = install.App.Type
|
||||||
if rawCompose, err := getUpgradeCompose(install, detail); err == nil {
|
if !readOnlyMode {
|
||||||
res.RawCompose = rawCompose
|
if rawCompose, err := getUpgradeCompose(install, detail); err == nil {
|
||||||
|
res.RawCompose = rawCompose
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &res, nil
|
return &res, nil
|
||||||
}
|
}
|
||||||
@@ -958,20 +971,25 @@ func updateInstallInfoInDB(appKey, appName, param string, value interface{}) err
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AppInstallService) GetAppInstallInfo(installID uint) (*response.AppInstallInfo, error) {
|
func (a *AppInstallService) GetAppInstallInfo(installID uint, readOnly ...bool) (*response.AppInstallInfo, error) {
|
||||||
appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(installID))
|
appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(installID))
|
||||||
if appInstall.ID == 0 {
|
if appInstall.ID == 0 {
|
||||||
return &response.AppInstallInfo{
|
return &response.AppInstallInfo{
|
||||||
Status: constant.StatusDeleted,
|
Status: constant.StatusDeleted,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
_ = syncAppInstallStatus(&appInstall, false)
|
if !isDemoReadOnly(readOnly...) {
|
||||||
|
_ = syncAppInstallStatus(&appInstall, false)
|
||||||
|
}
|
||||||
appInstall, _ = appInstallRepo.GetFirst(repo.WithByID(installID))
|
appInstall, _ = appInstallRepo.GetFirst(repo.WithByID(installID))
|
||||||
var envMap map[string]interface{}
|
var envMap map[string]interface{}
|
||||||
err := json.Unmarshal([]byte(appInstall.Env), &envMap)
|
err := json.Unmarshal([]byte(appInstall.Env), &envMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if isDemoReadOnly(readOnly...) {
|
||||||
|
redactSensitiveValues(envMap)
|
||||||
|
}
|
||||||
res := &response.AppInstallInfo{
|
res := &response.AppInstallInfo{
|
||||||
ID: appInstall.ID,
|
ID: appInstall.ID,
|
||||||
Name: appInstall.Name,
|
Name: appInstall.Name,
|
||||||
|
|||||||
@@ -0,0 +1,927 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type appUpgradePhase int
|
||||||
|
|
||||||
|
const (
|
||||||
|
appUpgradePreparing appUpgradePhase = iota
|
||||||
|
appUpgradePrepared
|
||||||
|
appUpgradeStopped
|
||||||
|
appUpgradeBackedUp
|
||||||
|
appUpgradeDown
|
||||||
|
appUpgradeMutated
|
||||||
|
appUpgradeStarted
|
||||||
|
appUpgradeReady
|
||||||
|
appUpgradeCommitted
|
||||||
|
)
|
||||||
|
|
||||||
|
const composeServiceLabel = "com.docker.compose.service"
|
||||||
|
|
||||||
|
var appUpgradeLocks sync.Map
|
||||||
|
|
||||||
|
type appUpgradeSnapshot interface {
|
||||||
|
Restore() error
|
||||||
|
Cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
type upgradeFileSnapshot struct {
|
||||||
|
installPath string
|
||||||
|
backupPath string
|
||||||
|
paths []string
|
||||||
|
existing map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type appUpgradeContext struct {
|
||||||
|
req request.AppInstallUpgrade
|
||||||
|
original model.AppInstall
|
||||||
|
candidate model.AppInstall
|
||||||
|
detail model.AppDetail
|
||||||
|
|
||||||
|
phase appUpgradePhase
|
||||||
|
stopAttempted bool
|
||||||
|
downAttempted bool
|
||||||
|
rollbackErr error
|
||||||
|
|
||||||
|
detailDir string
|
||||||
|
stageDir string
|
||||||
|
envContent []byte
|
||||||
|
oldEnvContent []byte
|
||||||
|
oldDockerCompose string
|
||||||
|
oldImageIDs []appImageID
|
||||||
|
backupFile string
|
||||||
|
snapshot appUpgradeSnapshot
|
||||||
|
createdPaths []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func upgradeInstall(req request.AppInstallUpgrade) error {
|
||||||
|
install, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if install.Status == constant.StatusUpgrading {
|
||||||
|
return buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
|
if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, install.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, loaded := appUpgradeLocks.LoadOrStore(install.ID, struct{}{}); loaded {
|
||||||
|
return buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
|
releaseLock := true
|
||||||
|
defer func() {
|
||||||
|
if releaseLock {
|
||||||
|
appUpgradeLocks.Delete(install.ID)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
detail, err := appDetailRepo.GetFirst(repo.WithByID(req.DetailID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if install.App.Key == vllmAppKeyForUpgrade && !isVllmUpgradeVersionAllowed(install.Version, detail.Version, loadVllmImageFromEnv(install.Env)) {
|
||||||
|
return errors.New("vLLM can only upgrade within the same image type")
|
||||||
|
}
|
||||||
|
if install.Version == detail.Version {
|
||||||
|
return errors.New("two version is same")
|
||||||
|
}
|
||||||
|
|
||||||
|
upgradeTask, err := task.NewTaskWithOps(install.Name, task.TaskUpgrade, task.TaskScopeApp, req.TaskID, install.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ctx := &appUpgradeContext{
|
||||||
|
req: req,
|
||||||
|
original: install,
|
||||||
|
candidate: install,
|
||||||
|
detail: detail,
|
||||||
|
phase: appUpgradePreparing,
|
||||||
|
oldDockerCompose: install.DockerCompose,
|
||||||
|
}
|
||||||
|
upgradeTask.AddSubTaskWithOps(i18n.GetMsgByKey("UpgradePrepare"), ctx.prepare, nil, 0, 0)
|
||||||
|
upgradeTask.AddSubTaskWithOps(
|
||||||
|
task.GetTaskName(install.Name, task.TaskUpgrade, task.TaskScopeApp),
|
||||||
|
ctx.cutover,
|
||||||
|
func(t *task.Task) {
|
||||||
|
ctx.rollbackErr = ctx.rollback(t)
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
upgradingInstall := install
|
||||||
|
upgradingInstall.Status = constant.StatusUpgrading
|
||||||
|
upgradingInstall.Message = ""
|
||||||
|
if err = appInstallRepo.Save(context.Background(), &upgradingInstall); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseLock = false
|
||||||
|
go func() {
|
||||||
|
defer appUpgradeLocks.Delete(install.ID)
|
||||||
|
defer ctx.cleanup()
|
||||||
|
taskErr := upgradeTask.Execute()
|
||||||
|
if taskErr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ctx.rollbackErr != nil {
|
||||||
|
taskErr = fmt.Errorf("%w; %s: %v", taskErr, i18n.GetMsgByKey("UpgradeRollbackFailed"), ctx.rollbackErr)
|
||||||
|
upgradeTask.Task.ErrorMsg = taskErr.Error()
|
||||||
|
_ = repo.NewITaskRepo().Update(context.Background(), upgradeTask.Task)
|
||||||
|
}
|
||||||
|
if !ctx.stopAttempted || ctx.rollbackErr == nil {
|
||||||
|
restored := ctx.original
|
||||||
|
_ = appInstallRepo.Save(context.Background(), &restored)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
failed := ctx.original
|
||||||
|
failed.Status = constant.StatusUpgradeErr
|
||||||
|
failed.Message = taskErr.Error()
|
||||||
|
_ = appInstallRepo.Save(context.Background(), &failed)
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) prepare(t *task.Task) error {
|
||||||
|
fileOp := files.NewFileOp()
|
||||||
|
u.detailDir = path.Join(u.original.App.GetAppResourcePath(), u.detail.Version)
|
||||||
|
if u.original.App.Resource == constant.AppResourceRemote {
|
||||||
|
if err := downloadApp(u.original.App, u.detail, nil, t.Logger); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !fileOp.Stat(u.detailDir) {
|
||||||
|
return buserr.WithName("ErrFileNotFound", u.detailDir)
|
||||||
|
}
|
||||||
|
if u.detail.DockerCompose == "" {
|
||||||
|
composeContent, err := fileOp.GetContent(path.Join(u.detailDir, "docker-compose.yml"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u.detail.DockerCompose = string(composeContent)
|
||||||
|
_ = appDetailRepo.Update(context.Background(), u.detail)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(u.detail.DockerCompose) == "" && strings.TrimSpace(u.req.DockerCompose) == "" {
|
||||||
|
return buserr.WithName("ErrFileNotFound", "docker-compose.yml")
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
u.oldEnvContent, err = fileOp.GetContent(u.original.GetEnvPath())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u.stageDir, err = os.MkdirTemp(u.original.GetAppPath(), "."+u.original.Name+"-upgrade-")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = fileOp.CopyDirWithNewName(u.detailDir, u.stageDir, "."); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = copyUpgradeStageFile(u.original.GetPath(), u.stageDir, ".env"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if u.original.App.Key == constant.AppOpenclaw {
|
||||||
|
if err = copyUpgradeStageFile(u.original.GetPath(), u.stageDir, path.Join("data", "conf", "openclaw.json")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if u.original.App.Key == constant.AppOpenresty {
|
||||||
|
for _, relativePath := range []string{
|
||||||
|
nginxModuleBuildDir,
|
||||||
|
nginxModuleModulesDir,
|
||||||
|
path.Join(nginxModuleConfDir, nginxModuleEnabledConfDir),
|
||||||
|
} {
|
||||||
|
if err = copyUpgradeStageFile(u.original.GetPath(), u.stageDir, relativePath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stagedInstall := u.original
|
||||||
|
stagedInstall.Name = path.Base(u.stageDir)
|
||||||
|
stagedInstall.Version = u.detail.Version
|
||||||
|
stagedInstall.AppDetailId = u.req.DetailID
|
||||||
|
if stagedInstall.App.Key == vllmAppKeyForUpgrade {
|
||||||
|
envs := make(map[string]interface{})
|
||||||
|
if err = json.Unmarshal([]byte(stagedInstall.Env), &envs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
image := buildVllmUpgradeImage(loadVllmImageFromEnv(stagedInstall.Env), u.original.Version, u.detail.Version)
|
||||||
|
envs[vllmImageEnvKey] = image
|
||||||
|
paramBytes, marshalErr := json.Marshal(envs)
|
||||||
|
if marshalErr != nil {
|
||||||
|
return marshalErr
|
||||||
|
}
|
||||||
|
stagedInstall.Env = string(paramBytes)
|
||||||
|
}
|
||||||
|
if err = migrateOpenclawProtocolUpgrade(&stagedInstall, u.original.Version, u.detail.Version); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.candidate = stagedInstall
|
||||||
|
u.candidate.Name = u.original.Name
|
||||||
|
u.candidate.DockerCompose, err = renderUpgradeCompose(u.candidate, u.detail, u.req.DockerCompose)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(u.candidate.DockerCompose) == "" {
|
||||||
|
return buserr.WithName("ErrFileNotFound", "docker-compose.yml")
|
||||||
|
}
|
||||||
|
|
||||||
|
u.envContent, err = renderUpgradeEnv(&u.candidate, u.oldEnvContent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = writeUpgradeFile(path.Join(u.stageDir, ".env"), u.envContent, constant.FilePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = writeUpgradeFile(path.Join(u.stageDir, "docker-compose.yml"), []byte(u.candidate.DockerCompose), constant.FilePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
project, err := docker.GetComposeProject(u.original.Name, u.stageDir, []byte(u.candidate.DockerCompose), u.envContent, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hasBuild := false
|
||||||
|
for _, service := range project.Services {
|
||||||
|
if service.Image == "" && service.Build == nil {
|
||||||
|
return fmt.Errorf("compose service %s has neither image nor build configuration", service.Name)
|
||||||
|
}
|
||||||
|
hasBuild = hasBuild || service.Build != nil
|
||||||
|
}
|
||||||
|
if u.req.DeleteImage {
|
||||||
|
dockerClient, clientErr := docker.NewClient()
|
||||||
|
if clientErr != nil {
|
||||||
|
return clientErr
|
||||||
|
}
|
||||||
|
u.oldImageIDs, err = getAppImageIDsByCompose(dockerClient, u.oldEnvContent, []byte(u.oldDockerCompose))
|
||||||
|
dockerClient.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
images := make([]string, 0, len(project.Services))
|
||||||
|
for _, service := range project.Services {
|
||||||
|
if service.Image != "" {
|
||||||
|
images = append(images, service.Image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = prepareUpgradeImages(t, images, u.req.PullImage); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if u.candidate.App.Key == constant.AppOpenresty {
|
||||||
|
if err = u.prepareOpenresty(t, stagedInstall); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = verifyUpgradeImages(images); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if hasBuild {
|
||||||
|
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("TaskBuild"), i18n.GetMsgByKey("Image"))
|
||||||
|
t.LogStart(logStr)
|
||||||
|
if err = compose.BuildWithTask(path.Join(u.stageDir, "docker-compose.yml"), project.Name, t); err != nil {
|
||||||
|
t.LogFailedWithErr(logStr, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.LogSuccess(logStr)
|
||||||
|
if err = verifyUpgradeImages(images); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if u.original.App.Resource == constant.AppResourceRemote {
|
||||||
|
go RequestDownloadCallBack(u.detail.DownloadCallBackUrl)
|
||||||
|
}
|
||||||
|
u.phase = appUpgradePrepared
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) prepareOpenresty(t *task.Task, stagedInstall model.AppInstall) error {
|
||||||
|
fileOp := files.NewFileOp()
|
||||||
|
detailBuildDir := path.Join(u.detailDir, nginxModuleBuildDir)
|
||||||
|
installBuildDir := path.Join(u.stageDir, nginxModuleBuildDir)
|
||||||
|
if !fileOp.Stat(installBuildDir) {
|
||||||
|
if err := fileOp.CreateDir(installBuildDir, constant.DirPerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := copyAppDetailMissing(fileOp, detailBuildDir, installBuildDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := fileOp.DeleteDir(path.Join(installBuildDir, nginxModuleTmpDir)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := fileOp.CopyDir(path.Join(detailBuildDir, nginxModuleTmpDir), installBuildDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, fileName := range []string{"Dockerfile", "nginx.conf", "nginx.vh.default.conf"} {
|
||||||
|
if err := fileOp.CopyFile(path.Join(detailBuildDir, fileName), installBuildDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := syncNginxModuleBuilder(detailBuildDir, installBuildDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
targetCatalogSource := path.Join(detailBuildDir, nginxModuleCatalogFile)
|
||||||
|
if !fileOp.Stat(targetCatalogSource) {
|
||||||
|
return fmt.Errorf("target OpenResty module catalog not found: %s", targetCatalogSource)
|
||||||
|
}
|
||||||
|
targetCatalogPath := path.Join(installBuildDir, nginxModuleCatalogPendingFile)
|
||||||
|
if err := stageNginxModuleCatalog(targetCatalogSource, targetCatalogPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stagedInstall.Name = path.Base(u.stageDir)
|
||||||
|
stagedInstall.Version = u.candidate.Version
|
||||||
|
stagedInstall.Env = u.candidate.Env
|
||||||
|
stagedInstall.DockerCompose = u.candidate.DockerCompose
|
||||||
|
return buildNginx(t, stagedInstall, targetCatalogPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) cutover(t *task.Task) error {
|
||||||
|
u.stopAttempted = true
|
||||||
|
t.LogStart(i18n.GetMsgByKey("UpgradeStop"))
|
||||||
|
if out, err := compose.Stop(u.original.GetComposePath()); err != nil {
|
||||||
|
if out != "" {
|
||||||
|
err = fmt.Errorf("%s: %w", out, err)
|
||||||
|
}
|
||||||
|
t.LogFailedWithErr(i18n.GetMsgByKey("UpgradeStop"), err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.LogSuccess(i18n.GetMsgByKey("UpgradeStop"))
|
||||||
|
u.phase = appUpgradeStopped
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if u.original.App.Key == constant.AppOpenresty {
|
||||||
|
u.snapshot, err = createOpenrestyUpgradeSnapshot(u.original.GetPath())
|
||||||
|
} else {
|
||||||
|
snapshotPaths := []string{".env", "docker-compose.yml", "scripts"}
|
||||||
|
if u.original.App.Key == constant.AppOpenclaw {
|
||||||
|
snapshotPaths = append(snapshotPaths, path.Join("data", "conf", "openclaw.json"))
|
||||||
|
}
|
||||||
|
u.snapshot, err = createUpgradeFileSnapshot(u.original.GetPath(), snapshotPaths)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.req.Backup {
|
||||||
|
if err = u.backup(t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u.phase = appUpgradeBackedUp
|
||||||
|
} else {
|
||||||
|
t.Log(i18n.GetMsgByKey("UpgradeBackupDisabled"))
|
||||||
|
}
|
||||||
|
|
||||||
|
u.downAttempted = true
|
||||||
|
if out, downErr := compose.Down(u.original.GetComposePath()); downErr != nil {
|
||||||
|
if out != "" {
|
||||||
|
downErr = fmt.Errorf("%s: %w", out, downErr)
|
||||||
|
}
|
||||||
|
return downErr
|
||||||
|
}
|
||||||
|
u.phase = appUpgradeDown
|
||||||
|
|
||||||
|
u.phase = appUpgradeMutated
|
||||||
|
if err = u.applyStagedFiles(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = writeUpgradeFile(u.original.GetEnvPath(), u.envContent, constant.FilePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = runScript(t, &u.candidate, "upgrade"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = writeUpgradeFile(u.original.GetComposePath(), []byte(u.candidate.DockerCompose), constant.FilePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("Run"), i18n.GetMsgByKey("App"))
|
||||||
|
t.LogStart(logStr)
|
||||||
|
if out, upErr := compose.UpWithoutBuild(u.original.GetComposePath()); upErr != nil {
|
||||||
|
if out != "" {
|
||||||
|
upErr = fmt.Errorf("%s: %w", out, upErr)
|
||||||
|
}
|
||||||
|
t.LogFailedWithErr(logStr, upErr)
|
||||||
|
return upErr
|
||||||
|
}
|
||||||
|
t.LogSuccess(logStr)
|
||||||
|
u.phase = appUpgradeStarted
|
||||||
|
|
||||||
|
t.LogStart(i18n.GetMsgByKey("UpgradeWaitReady"))
|
||||||
|
containerNames, err := waitAppContainersReady(context.Background(), u.candidate)
|
||||||
|
if err != nil {
|
||||||
|
t.LogFailedWithErr(i18n.GetMsgByKey("UpgradeWaitReady"), err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.LogSuccess(i18n.GetMsgByKey("UpgradeWaitReady"))
|
||||||
|
u.phase = appUpgradeReady
|
||||||
|
u.candidate.ContainerName = strings.Join(containerNames, ",")
|
||||||
|
u.candidate.Status = constant.StatusRunning
|
||||||
|
u.candidate.Message = ""
|
||||||
|
|
||||||
|
if u.candidate.App.Key == constant.AppOpenresty {
|
||||||
|
liveCatalogPath := path.Join(u.candidate.GetPath(), nginxModuleBuildDir, nginxModuleCatalogPendingFile)
|
||||||
|
if err = commitStaticNginxModuleBuilds(u.candidate, liveCatalogPath, t); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
activeCatalogPath := path.Join(u.candidate.GetPath(), nginxModuleBuildDir, nginxModuleCatalogFile)
|
||||||
|
if err = activateNginxModuleCatalogAndCommit(liveCatalogPath, activeCatalogPath, func() error {
|
||||||
|
return appInstallRepo.Save(context.Background(), &u.candidate)
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if err = appInstallRepo.Save(context.Background(), &u.candidate); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u.phase = appUpgradeCommitted
|
||||||
|
u.deleteOldImages(t)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) backup(t *task.Task) error {
|
||||||
|
fileName := fmt.Sprintf("upgrade_backup_%s_%s.tar.gz", u.original.Name, time.Now().Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
|
||||||
|
record, err := backupAppWithParentTask(&u.original, t, fileName)
|
||||||
|
if err != nil {
|
||||||
|
return buserr.WithNameAndErr("ErrAppBackup", u.original.Name, err)
|
||||||
|
}
|
||||||
|
u.backupFile = path.Join(global.Dir.LocalBackupDir, record.FileDir, record.FileName)
|
||||||
|
info, err := os.Stat(u.backupFile)
|
||||||
|
if err != nil || info.Size() == 0 || record.Status != constant.StatusSuccess {
|
||||||
|
if err == nil {
|
||||||
|
err = errors.New("backup archive is empty or incomplete")
|
||||||
|
}
|
||||||
|
markBackupFailed(record.ID, err)
|
||||||
|
return buserr.WithNameAndErr("ErrAppBackup", u.original.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
backupRecordService := NewIBackupRecordService()
|
||||||
|
backups, _ := backupRecordService.ListAppRecords(u.original.App.Key, u.original.Name, "upgrade_backup")
|
||||||
|
if len(backups) > 3 {
|
||||||
|
deleteIDs := make([]uint, 0, len(backups)-3)
|
||||||
|
for _, backup := range backups[:len(backups)-3] {
|
||||||
|
deleteIDs = append(deleteIDs, backup.ID)
|
||||||
|
}
|
||||||
|
_ = backupRecordService.BatchDeleteRecord(deleteIDs)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) applyStagedFiles() error {
|
||||||
|
fileOp := files.NewFileOp()
|
||||||
|
if err := copyAppDetailMissingTracked(fileOp, u.detailDir, u.original.GetPath(), &u.createdPaths); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := replaceUpgradePath(u.stageDir, u.original.GetPath(), "scripts"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if u.original.App.Key == constant.AppOpenclaw {
|
||||||
|
if err := replaceUpgradePath(u.stageDir, u.original.GetPath(), path.Join("data", "conf", "openclaw.json")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if u.original.App.Key == constant.AppOpenresty {
|
||||||
|
for _, relativePath := range []string{
|
||||||
|
nginxModuleBuildDir,
|
||||||
|
nginxModuleModulesDir,
|
||||||
|
path.Join(nginxModuleConfDir, nginxModuleEnabledConfDir),
|
||||||
|
} {
|
||||||
|
if err := replaceUpgradePath(u.stageDir, u.original.GetPath(), relativePath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
|
||||||
|
if !u.stopAttempted {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
logStr := i18n.GetWithName("AppRecover", u.original.Name)
|
||||||
|
t.LogStart(logStr)
|
||||||
|
defer func() {
|
||||||
|
if rollbackErr != nil {
|
||||||
|
t.LogFailedWithErr(logStr, rollbackErr)
|
||||||
|
} else {
|
||||||
|
t.LogSuccess(logStr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if !u.downAttempted {
|
||||||
|
if out, err := compose.Operate(u.original.GetComposePath(), "start"); err != nil {
|
||||||
|
if out != "" {
|
||||||
|
err = fmt.Errorf("%s: %w", out, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return u.finishRollback()
|
||||||
|
}
|
||||||
|
if u.phase < appUpgradeMutated {
|
||||||
|
if out, err := compose.UpWithoutBuild(u.original.GetComposePath()); err != nil {
|
||||||
|
if out != "" {
|
||||||
|
err = fmt.Errorf("%s: %w", out, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return u.finishRollback()
|
||||||
|
}
|
||||||
|
|
||||||
|
if out, err := compose.Down(u.original.GetComposePath()); err != nil {
|
||||||
|
if out != "" {
|
||||||
|
err = fmt.Errorf("%s: %w", out, err)
|
||||||
|
}
|
||||||
|
rollbackErr = err
|
||||||
|
}
|
||||||
|
if u.backupFile != "" {
|
||||||
|
_ = u.restoreManagedFiles()
|
||||||
|
if err := handleAppRecover(&u.original, t, u.backupFile, true, "", ""); err != nil {
|
||||||
|
_, _ = compose.UpWithoutBuild(u.original.GetComposePath())
|
||||||
|
return errors.Join(rollbackErr, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := u.restoreManagedFiles(); err != nil {
|
||||||
|
return errors.Join(rollbackErr, err)
|
||||||
|
}
|
||||||
|
if out, err := compose.UpWithoutBuild(u.original.GetComposePath()); err != nil {
|
||||||
|
if out != "" {
|
||||||
|
err = fmt.Errorf("%s: %w", out, err)
|
||||||
|
}
|
||||||
|
return errors.Join(rollbackErr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.Join(rollbackErr, u.finishRollback())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) finishRollback() error {
|
||||||
|
if _, err := waitAppContainersReady(context.Background(), u.original); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
restored := u.original
|
||||||
|
if err := appInstallRepo.Save(context.Background(), &restored); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) restoreManagedFiles() error {
|
||||||
|
var restoreErr error
|
||||||
|
if u.snapshot != nil {
|
||||||
|
restoreErr = u.snapshot.Restore()
|
||||||
|
}
|
||||||
|
for index := len(u.createdPaths) - 1; index >= 0; index-- {
|
||||||
|
if err := os.RemoveAll(u.createdPaths[index]); err != nil {
|
||||||
|
restoreErr = errors.Join(restoreErr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return restoreErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) deleteOldImages(t *task.Task) {
|
||||||
|
if !u.req.DeleteImage {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
excludeImages, err := docker.GetImagesFromDockerCompose(u.envContent, []byte(u.candidate.DockerCompose))
|
||||||
|
if err != nil {
|
||||||
|
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dockerClient, err := docker.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer dockerClient.Close()
|
||||||
|
if err = deleteAppImagesByIDs(t, dockerClient, u.oldImageIDs, excludeImages); err != nil {
|
||||||
|
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUpgradeContext) cleanup() {
|
||||||
|
if u.snapshot != nil {
|
||||||
|
u.snapshot.Cleanup()
|
||||||
|
}
|
||||||
|
if u.stageDir != "" {
|
||||||
|
_ = os.RemoveAll(u.stageDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type upgradeImageClient interface {
|
||||||
|
PullImageWithProcess(*task.Task, string) error
|
||||||
|
ImageExists(string) (bool, error)
|
||||||
|
Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareUpgradeImages(t *task.Task, images []string, pull bool) error {
|
||||||
|
dockerClient, err := docker.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return prepareUpgradeImagesWithClient(t, dockerClient, images, pull)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareUpgradeImagesWithClient(t *task.Task, dockerClient upgradeImageClient, images []string, pull bool) error {
|
||||||
|
defer dockerClient.Close()
|
||||||
|
seen := make(map[string]struct{}, len(images))
|
||||||
|
for _, image := range images {
|
||||||
|
image = strings.TrimSpace(image)
|
||||||
|
if image == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[image]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[image] = struct{}{}
|
||||||
|
if pull {
|
||||||
|
if t != nil {
|
||||||
|
t.Log(i18n.GetWithName("PullImageStart", image))
|
||||||
|
}
|
||||||
|
if pullErr := dockerClient.PullImageWithProcess(t, image); pullErr != nil {
|
||||||
|
if exists, _ := dockerClient.ImageExists(image); exists {
|
||||||
|
if t != nil {
|
||||||
|
t.Log(i18n.GetMsgByKey("UseExistImage"))
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return buserr.WithNameAndErr("ErrDockerPullImage", "", pullErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exists, inspectErr := dockerClient.ImageExists(image)
|
||||||
|
if inspectErr != nil || !exists {
|
||||||
|
return buserr.WithNameAndErr("ErrDockerPullImage", "", fmt.Errorf("image %s is not available locally: %v", image, inspectErr))
|
||||||
|
}
|
||||||
|
if pull && t != nil {
|
||||||
|
t.LogSuccess(i18n.GetMsgByKey("PullImage"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyUpgradeImages(images []string) error {
|
||||||
|
dockerClient, err := docker.NewClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dockerClient.Close()
|
||||||
|
for _, image := range images {
|
||||||
|
exists, inspectErr := dockerClient.ImageExists(image)
|
||||||
|
if inspectErr != nil || !exists {
|
||||||
|
return buserr.WithNameAndErr("ErrDockerPullImage", "", fmt.Errorf("image %s is not available locally: %v", image, inspectErr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderUpgradeEnv(install *model.AppInstall, original []byte) ([]byte, error) {
|
||||||
|
originalEnv := make(map[string]string)
|
||||||
|
if len(original) > 0 {
|
||||||
|
var err error
|
||||||
|
originalEnv, err = godotenv.UnmarshalBytes(original)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
params := make(map[string]string, len(originalEnv))
|
||||||
|
maps.Copy(params, originalEnv)
|
||||||
|
envs := make(map[string]interface{})
|
||||||
|
if err := json.Unmarshal([]byte(install.Env), &envs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
handleMap(envs, params)
|
||||||
|
if install.App.Key == constant.AppOpenresty {
|
||||||
|
for _, key := range []string{"CONTAINER_PACKAGE_URL", "RESTY_ADD_PACKAGE_BUILDDEPS", "RESTY_CONFIG_OPTIONS_MORE"} {
|
||||||
|
if value, ok := originalEnv[key]; ok {
|
||||||
|
params[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if websiteDir := strings.TrimSpace(originalEnv["WEBSITE_DIR"]); websiteDir != "" {
|
||||||
|
params["WEBSITE_DIR"] = websiteDir
|
||||||
|
}
|
||||||
|
websiteDir := strings.TrimSpace(params["WEBSITE_DIR"])
|
||||||
|
if websiteDir == "" {
|
||||||
|
websiteDir = NewISettingService().GetWebsiteDir()
|
||||||
|
}
|
||||||
|
if !path.IsAbs(websiteDir) {
|
||||||
|
websiteDir = path.Join(global.Dir.DataDir, websiteDir)
|
||||||
|
}
|
||||||
|
params["WEBSITE_DIR"] = websiteDir
|
||||||
|
envs["WEBSITE_DIR"] = websiteDir
|
||||||
|
content, marshalErr := json.Marshal(envs)
|
||||||
|
if marshalErr != nil {
|
||||||
|
return nil, marshalErr
|
||||||
|
}
|
||||||
|
install.Env = string(content)
|
||||||
|
}
|
||||||
|
content, err := godotenv.Marshal(params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []byte(content), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderUpgradeCompose(install model.AppInstall, detail model.AppDetail, customCompose string) (string, error) {
|
||||||
|
if customCompose != "" {
|
||||||
|
return customCompose, nil
|
||||||
|
}
|
||||||
|
if install.App.Key == vllmAppKeyForUpgrade {
|
||||||
|
return install.DockerCompose, nil
|
||||||
|
}
|
||||||
|
return getUpgradeCompose(install, detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeUpgradeFile(filePath string, content []byte, mode os.FileMode) error {
|
||||||
|
tmp, err := os.CreateTemp(path.Dir(filePath), "."+path.Base(filePath)+".*")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tmpPath := tmp.Name()
|
||||||
|
defer os.Remove(tmpPath)
|
||||||
|
if err = tmp.Chmod(mode); err == nil {
|
||||||
|
_, err = tmp.Write(content)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = tmp.Sync()
|
||||||
|
}
|
||||||
|
if closeErr := tmp.Close(); err == nil {
|
||||||
|
err = closeErr
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Rename(tmpPath, filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyUpgradeStageFile(sourceRoot, targetRoot, relativePath string) error {
|
||||||
|
source := path.Join(sourceRoot, relativePath)
|
||||||
|
if _, err := os.Stat(source); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
target := path.Join(targetRoot, relativePath)
|
||||||
|
_ = os.RemoveAll(target)
|
||||||
|
return copyOpenrestyUpgradeSnapshotEntry(source, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceUpgradePath(sourceRoot, targetRoot, relativePath string) error {
|
||||||
|
source := path.Join(sourceRoot, relativePath)
|
||||||
|
if _, err := os.Stat(source); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
target := path.Join(targetRoot, relativePath)
|
||||||
|
if err := os.RemoveAll(target); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return copyOpenrestyUpgradeSnapshotEntry(source, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createUpgradeFileSnapshot(installPath string, paths []string) (*upgradeFileSnapshot, error) {
|
||||||
|
backupPath, err := os.MkdirTemp("", "1panel-app-upgrade-*")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
snapshot := &upgradeFileSnapshot{
|
||||||
|
installPath: installPath,
|
||||||
|
backupPath: backupPath,
|
||||||
|
paths: paths,
|
||||||
|
existing: make(map[string]bool, len(paths)),
|
||||||
|
}
|
||||||
|
for _, relativePath := range paths {
|
||||||
|
source := path.Join(installPath, relativePath)
|
||||||
|
if _, err = os.Stat(source); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
snapshot.Cleanup()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
snapshot.existing[relativePath] = true
|
||||||
|
if err = copyOpenrestyUpgradeSnapshotEntry(source, path.Join(backupPath, relativePath)); err != nil {
|
||||||
|
snapshot.Cleanup()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return snapshot, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *upgradeFileSnapshot) Restore() error {
|
||||||
|
for _, relativePath := range s.paths {
|
||||||
|
target := path.Join(s.installPath, relativePath)
|
||||||
|
if err := os.RemoveAll(target); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !s.existing[relativePath] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := copyOpenrestyUpgradeSnapshotEntry(path.Join(s.backupPath, relativePath), target); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *upgradeFileSnapshot) Cleanup() {
|
||||||
|
if s != nil && s.backupPath != "" {
|
||||||
|
_ = os.RemoveAll(s.backupPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type appContainerReadinessClient interface {
|
||||||
|
ContainerList(context.Context, container.ListOptions) ([]container.Summary, error)
|
||||||
|
ContainerInspect(context.Context, string) (container.InspectResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitAppContainersReady(ctx context.Context, install model.AppInstall) ([]string, error) {
|
||||||
|
client, err := docker.NewDockerClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
return waitAppContainersReadyWithClient(ctx, client, install)
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitAppContainersReadyWithClient(ctx context.Context, client appContainerReadinessClient, install model.AppInstall) ([]string, error) {
|
||||||
|
envContent, err := os.ReadFile(install.GetEnvPath())
|
||||||
|
if err != nil {
|
||||||
|
envContent, err = renderUpgradeEnv(&install, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
project, err := docker.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), envContent, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
expectedServices := make(map[string]struct{})
|
||||||
|
for _, service := range project.Services {
|
||||||
|
if !skipCheckStatus(service) {
|
||||||
|
expectedServices[service.Name] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(expectedServices) == 0 {
|
||||||
|
return strings.Split(install.ContainerName, ","), nil
|
||||||
|
}
|
||||||
|
options := container.ListOptions{
|
||||||
|
All: true,
|
||||||
|
Filters: filters.NewArgs(
|
||||||
|
filters.Arg("label", composeWorkdirLabel+"="+install.GetPath()),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
containers, err := client.ContainerList(ctx, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
foundServices := make(map[string]bool, len(expectedServices))
|
||||||
|
containerNames := make([]string, 0, len(containers))
|
||||||
|
for _, item := range containers {
|
||||||
|
serviceName := item.Labels[composeServiceLabel]
|
||||||
|
if _, ok := expectedServices[serviceName]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = waitContainerReady(ctx, client, item.ID); err != nil {
|
||||||
|
return nil, fmt.Errorf("container %s is not ready: %w", serviceName, err)
|
||||||
|
}
|
||||||
|
foundServices[serviceName] = true
|
||||||
|
if len(item.Names) > 0 {
|
||||||
|
containerNames = append(containerNames, strings.TrimPrefix(item.Names[0], "/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for serviceName := range expectedServices {
|
||||||
|
if !foundServices[serviceName] {
|
||||||
|
return nil, fmt.Errorf("container for service %s was not created", serviceName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(containerNames)
|
||||||
|
return containerNames, nil
|
||||||
|
}
|
||||||
+77
-416
@@ -9,7 +9,6 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -54,6 +53,62 @@ var (
|
|||||||
Delete DatabaseOp = "delete"
|
Delete DatabaseOp = "delete"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func isDemoReadOnly(readOnly ...bool) bool {
|
||||||
|
return global.CONF.Base.IsDemo || len(readOnly) > 0 && readOnly[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeConfigKey(key string) string {
|
||||||
|
return strings.ToLower(strings.NewReplacer("_", "", "-", "", ".", "", " ", "").Replace(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSensitiveConfigKey(key string) bool {
|
||||||
|
normalized := normalizeConfigKey(key)
|
||||||
|
for _, marker := range []string{"password", "passwd", "passphrase", "secret", "token", "credential", "privatekey", "authorization"} {
|
||||||
|
if strings.Contains(normalized, marker) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return normalized == "key" || strings.HasSuffix(normalized, "key")
|
||||||
|
}
|
||||||
|
|
||||||
|
func redactSensitiveValues(values map[string]interface{}) {
|
||||||
|
redactSensitiveData(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
func redactSensitiveData(value interface{}) {
|
||||||
|
switch data := value.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
for key, item := range data {
|
||||||
|
if isSensitiveConfigKey(key) {
|
||||||
|
data[key] = ""
|
||||||
|
} else {
|
||||||
|
redactSensitiveData(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
for _, item := range data {
|
||||||
|
redactSensitiveData(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func redactSensitiveAppParams(params []response.AppParam) {
|
||||||
|
for i := range params {
|
||||||
|
if strings.EqualFold(params[i].Type, "password") || isSensitiveConfigKey(params[i].Key) {
|
||||||
|
params[i].Value = ""
|
||||||
|
params[i].ShowValue = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func redactSensitiveEnvironments(environments []request.Environment) {
|
||||||
|
for i := range environments {
|
||||||
|
if isSensitiveConfigKey(environments[i].Key) {
|
||||||
|
environments[i].Value = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func checkPort(key string, params map[string]interface{}) (int, error) {
|
func checkPort(key string, params map[string]interface{}) (int, error) {
|
||||||
port, ok := params[key]
|
port, ok := params[key]
|
||||||
if ok {
|
if ok {
|
||||||
@@ -783,416 +838,6 @@ func buildNginx(parentTask *task.Task, nginxInstall model.AppInstall, catalogPat
|
|||||||
return commitNginxModuleBuilds(nginxInstall, previousModules, modules, false, catalogPath)
|
return commitNginxModuleBuilds(nginxInstall, previousModules, modules, false, catalogPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func upgradeInstall(req request.AppInstallUpgrade) error {
|
|
||||||
install, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
originalInstall := install
|
|
||||||
oldVersion := install.Version
|
|
||||||
detail, err := appDetailRepo.GetFirst(repo.WithByID(req.DetailID))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if install.App.Key == vllmAppKeyForUpgrade && !isVllmUpgradeVersionAllowed(install.Version, detail.Version, loadVllmImageFromEnv(install.Env)) {
|
|
||||||
return errors.New("vLLM can only upgrade within the same image type")
|
|
||||||
}
|
|
||||||
if install.Version == detail.Version {
|
|
||||||
return errors.New("two version is same")
|
|
||||||
}
|
|
||||||
|
|
||||||
upgradeTask, err := task.NewTaskWithOps(install.Name, task.TaskUpgrade, task.TaskScopeApp, req.TaskID, install.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
install.Status = constant.StatusUpgrading
|
|
||||||
|
|
||||||
var (
|
|
||||||
upErr error
|
|
||||||
backupFile string
|
|
||||||
nginxUpgradeSnapshot *openrestyUpgradeSnapshot
|
|
||||||
)
|
|
||||||
backUpApp := func(t *task.Task) error {
|
|
||||||
backupService := NewIBackupService()
|
|
||||||
backupRecordService := NewIBackupRecordService()
|
|
||||||
fileName := fmt.Sprintf("upgrade_backup_%s_%s.tar.gz", install.Name, time.Now().Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
|
|
||||||
backupRecord, err := backupService.AppBackup(dto.CommonBackup{Name: install.App.Key, DetailName: install.Name, FileName: fileName})
|
|
||||||
if err == nil {
|
|
||||||
backups, _ := backupRecordService.ListAppRecords(install.App.Key, install.Name, "upgrade_backup")
|
|
||||||
if len(backups) > 3 {
|
|
||||||
backupsToDelete := backups[:len(backups)-3]
|
|
||||||
var deleteIDs []uint
|
|
||||||
for _, backup := range backupsToDelete {
|
|
||||||
deleteIDs = append(deleteIDs, backup.ID)
|
|
||||||
}
|
|
||||||
_ = backupRecordService.BatchDeleteRecord(deleteIDs)
|
|
||||||
}
|
|
||||||
backupFile = path.Join(global.Dir.LocalBackupDir, backupRecord.FileDir, backupRecord.FileName)
|
|
||||||
} else {
|
|
||||||
return buserr.WithNameAndErr("ErrAppBackup", install.Name, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if req.Backup {
|
|
||||||
upgradeTask.AddSubTask(task.GetTaskName(install.Name, task.TaskBackup, task.TaskScopeApp), backUpApp, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
upgradeApp := func(t *task.Task) error {
|
|
||||||
fileOp := files.NewFileOp()
|
|
||||||
detailDir := path.Join(global.Dir.ResourceDir, "apps", install.App.Resource, install.App.Key, detail.Version)
|
|
||||||
if install.App.Resource == constant.AppResourceRemote {
|
|
||||||
if err = downloadApp(install.App, detail, &install, t.Logger); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if detail.DockerCompose == "" {
|
|
||||||
composeDetail, err := fileOp.GetContent(path.Join(detailDir, "docker-compose.yml"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
detail.DockerCompose = string(composeDetail)
|
|
||||||
_ = appDetailRepo.Update(context.Background(), detail)
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
RequestDownloadCallBack(detail.DownloadCallBackUrl)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
if install.App.Resource == constant.AppResourceLocal {
|
|
||||||
detailDir = path.Join(global.Dir.ResourceDir, "apps", "local", strings.TrimPrefix(install.App.Key, "local"), detail.Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := fileOp.GetContent(install.GetEnvPath())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
oldEnvContent := append([]byte(nil), content...)
|
|
||||||
oldDockerCompose := install.DockerCompose
|
|
||||||
targetNginxCatalogPath := ""
|
|
||||||
if install.App.Key == constant.AppOpenresty {
|
|
||||||
nginxUpgradeSnapshot, err = createOpenrestyUpgradeSnapshot(install.GetPath())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if install.App.Key == vllmAppKeyForUpgrade {
|
|
||||||
envs := make(map[string]interface{})
|
|
||||||
if err = json.Unmarshal([]byte(install.Env), &envs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
image := buildVllmUpgradeImage(loadVllmImageFromEnv(install.Env), oldVersion, detail.Version)
|
|
||||||
envs[vllmImageEnvKey] = image
|
|
||||||
paramByte, err := json.Marshal(envs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
install.Env = string(paramByte)
|
|
||||||
content = setVllmImageInEnvContent(content, image)
|
|
||||||
}
|
|
||||||
_ = copyAppDetailMissing(fileOp, detailDir, install.GetPath())
|
|
||||||
if install.App.Key == constant.AppOpenresty {
|
|
||||||
installBuildDir := path.Join(install.GetPath(), nginxModuleBuildDir)
|
|
||||||
detailBuildDir := path.Join(detailDir, nginxModuleBuildDir)
|
|
||||||
if !fileOp.Stat(installBuildDir) {
|
|
||||||
if err := fileOp.CreateDir(installBuildDir, constant.DirPerm); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := fileOp.DeleteDir(path.Join(installBuildDir, nginxModuleTmpDir)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := fileOp.CopyDir(path.Join(detailBuildDir, nginxModuleTmpDir), installBuildDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := fileOp.CopyFile(path.Join(detailBuildDir, "Dockerfile"), installBuildDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := syncNginxModuleBuilder(detailBuildDir, installBuildDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
targetCatalogSource := path.Join(detailBuildDir, nginxModuleCatalogFile)
|
|
||||||
if !fileOp.Stat(targetCatalogSource) {
|
|
||||||
return fmt.Errorf("target OpenResty module catalog not found: %s", targetCatalogSource)
|
|
||||||
}
|
|
||||||
targetNginxCatalogPath = path.Join(installBuildDir, nginxModuleCatalogPendingFile)
|
|
||||||
if err := stageNginxModuleCatalog(targetCatalogSource, targetNginxCatalogPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := fileOp.CopyFile(path.Join(detailBuildDir, "nginx.conf"), installBuildDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := fileOp.CopyFile(path.Join(detailBuildDir, "nginx.vh.default.conf"), installBuildDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sourceScripts := path.Join(detailDir, "scripts")
|
|
||||||
if fileOp.Stat(sourceScripts) {
|
|
||||||
dstScripts := path.Join(install.GetPath(), "scripts")
|
|
||||||
_ = fileOp.DeleteDir(dstScripts)
|
|
||||||
_ = fileOp.CreateDir(dstScripts, constant.DirPerm)
|
|
||||||
scriptCmd := exec.Command("cp", "-rf", sourceScripts+"/.", dstScripts+"/")
|
|
||||||
_, _ = scriptCmd.CombinedOutput()
|
|
||||||
}
|
|
||||||
|
|
||||||
var newCompose string
|
|
||||||
if err = migrateOpenclawProtocolUpgrade(&install, oldVersion, detail.Version); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if req.DockerCompose == "" {
|
|
||||||
if install.App.Key == vllmAppKeyForUpgrade {
|
|
||||||
newCompose = install.DockerCompose
|
|
||||||
} else {
|
|
||||||
newCompose, err = getUpgradeCompose(install, detail)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newCompose = req.DockerCompose
|
|
||||||
}
|
|
||||||
|
|
||||||
install.DockerCompose = newCompose
|
|
||||||
install.Version = detail.Version
|
|
||||||
install.AppDetailId = req.DetailID
|
|
||||||
|
|
||||||
var oldImageIDs []appImageID
|
|
||||||
if req.DeleteImage {
|
|
||||||
dockerCLi, err := docker.NewClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
oldImageIDs, err = getAppImageIDsByCompose(dockerCLi, oldEnvContent, []byte(oldDockerCompose))
|
|
||||||
dockerCLi.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.PullImage {
|
|
||||||
images, err := docker.GetImagesFromDockerCompose(content, []byte(install.DockerCompose))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dockerCLi, err := docker.NewClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer dockerCLi.Close()
|
|
||||||
for _, image := range images {
|
|
||||||
t.Log(i18n.GetWithName("PullImageStart", image))
|
|
||||||
if pullErr := dockerCLi.PullImageWithProcess(t, image); pullErr != nil {
|
|
||||||
if exist, _ := dockerCLi.ImageExists(image); exist {
|
|
||||||
t.Log(i18n.GetMsgByKey("UseExistImage"))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return buserr.WithNameAndErr("ErrDockerPullImage", "", pullErr)
|
|
||||||
}
|
|
||||||
exist, err := dockerCLi.ImageExists(image)
|
|
||||||
if err != nil || !exist {
|
|
||||||
return buserr.WithNameAndErr("ErrDockerPullImage", "", fmt.Errorf("image %s does not exist after pull: %v", image, err))
|
|
||||||
}
|
|
||||||
t.LogSuccess(i18n.GetMsgByKey("PullImage"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if install.App.Key == constant.AppOpenresty {
|
|
||||||
modules, moduleErr := loadNginxModulesWithCatalog(install, targetNginxCatalogPath)
|
|
||||||
if moduleErr != nil {
|
|
||||||
return moduleErr
|
|
||||||
}
|
|
||||||
// Build dynamic modules for the target version before stopping the
|
|
||||||
// current container. Static modules retain the full rebuild path.
|
|
||||||
if !hasEnabledStaticNginxModules(modules) {
|
|
||||||
previousModules := cloneNginxModules(modules)
|
|
||||||
modules, moduleErr = buildDynamicNginxModules(install, modules, nil, false, "", targetNginxCatalogPath, t)
|
|
||||||
if moduleErr != nil {
|
|
||||||
return moduleErr
|
|
||||||
}
|
|
||||||
if moduleErr = saveNginxModulesWithCatalog(install, modules, targetNginxCatalogPath); moduleErr != nil {
|
|
||||||
removeNginxModuleOutputsNotReferenced(install, modules, previousModules)
|
|
||||||
return moduleErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if out, err := compose.Down(install.GetComposePath()); err != nil {
|
|
||||||
if out != "" {
|
|
||||||
upErr = errors.New(out)
|
|
||||||
return upErr
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
envs := make(map[string]interface{})
|
|
||||||
if err = json.Unmarshal([]byte(install.Env), &envs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
envParams := make(map[string]string, len(envs))
|
|
||||||
if install.App.Key == constant.AppOpenresty {
|
|
||||||
packageUrl, _ := env.GetEnvValueByKey(install.GetEnvPath(), "CONTAINER_PACKAGE_URL")
|
|
||||||
addPackage, _ := env.GetEnvValueByKey(install.GetEnvPath(), "RESTY_ADD_PACKAGE_BUILDDEPS")
|
|
||||||
options, _ := env.GetEnvValueByKey(install.GetEnvPath(), "RESTY_CONFIG_OPTIONS_MORE")
|
|
||||||
envParams["CONTAINER_PACKAGE_URL"] = packageUrl
|
|
||||||
envParams["RESTY_ADD_PACKAGE_BUILDDEPS"] = addPackage
|
|
||||||
envParams["RESTY_CONFIG_OPTIONS_MORE"] = options
|
|
||||||
}
|
|
||||||
handleMap(envs, envParams)
|
|
||||||
if err = env.Write(envParams, install.GetEnvPath()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = runScript(t, &install, "upgrade"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = fileOp.WriteFile(install.GetComposePath(), strings.NewReader(install.DockerCompose), constant.FilePerm); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if install.App.Key == constant.AppOpenresty {
|
|
||||||
if err = buildNginx(t, install, targetNginxCatalogPath); err != nil {
|
|
||||||
t.Log(err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("Run"), i18n.GetMsgByKey("App"))
|
|
||||||
t.Log(logStr)
|
|
||||||
if out, err := compose.Up(install.GetComposePath()); err != nil {
|
|
||||||
if out != "" {
|
|
||||||
return errors.New(out)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
t.LogSuccess(logStr)
|
|
||||||
install.Status = constant.StatusRunning
|
|
||||||
if install.App.Key == constant.AppOpenresty {
|
|
||||||
if err = commitStaticNginxModuleBuilds(install, targetNginxCatalogPath, t); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
activeCatalogPath := path.Join(install.GetPath(), nginxModuleBuildDir, nginxModuleCatalogFile)
|
|
||||||
if err = activateNginxModuleCatalogAndCommit(targetNginxCatalogPath, activeCatalogPath, func() error {
|
|
||||||
return appInstallRepo.Save(context.Background(), &install)
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err = appInstallRepo.Save(context.Background(), &install); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if nginxUpgradeSnapshot != nil {
|
|
||||||
nginxUpgradeSnapshot.Cleanup()
|
|
||||||
nginxUpgradeSnapshot = nil
|
|
||||||
}
|
|
||||||
if req.DeleteImage {
|
|
||||||
newEnvContent, err := fileOp.GetContent(install.GetEnvPath())
|
|
||||||
if err != nil {
|
|
||||||
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
excludeImages, err := docker.GetImagesFromDockerCompose(newEnvContent, []byte(install.DockerCompose))
|
|
||||||
if err != nil {
|
|
||||||
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
dockerCLi, err := docker.NewClient()
|
|
||||||
if err != nil {
|
|
||||||
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
defer dockerCLi.Close()
|
|
||||||
if err = deleteAppImagesByIDs(t, dockerCLi, oldImageIDs, excludeImages); err != nil {
|
|
||||||
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
rollBackApp := func(t *task.Task) {
|
|
||||||
if req.Backup {
|
|
||||||
t.Log(i18n.GetWithName("AppRecover", install.Name))
|
|
||||||
recoverErr := NewIBackupService().AppRecover(dto.CommonRecover{
|
|
||||||
Name: install.App.Key, DetailName: install.Name, Type: "app", DownloadAccountID: 1, File: backupFile,
|
|
||||||
})
|
|
||||||
if recoverErr == nil {
|
|
||||||
if nginxUpgradeSnapshot != nil {
|
|
||||||
nginxUpgradeSnapshot.Cleanup()
|
|
||||||
nginxUpgradeSnapshot = nil
|
|
||||||
}
|
|
||||||
t.LogSuccess(i18n.GetWithName("AppRecover", install.Name))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), recoverErr)
|
|
||||||
if install.App.Key != constant.AppOpenresty {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if install.App.Key == constant.AppOpenresty && nginxUpgradeSnapshot != nil {
|
|
||||||
if out, rollbackErr := compose.Down(install.GetComposePath()); rollbackErr != nil {
|
|
||||||
if out != "" {
|
|
||||||
rollbackErr = fmt.Errorf("%s: %w", out, rollbackErr)
|
|
||||||
}
|
|
||||||
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr)
|
|
||||||
}
|
|
||||||
if rollbackErr := nginxUpgradeSnapshot.Restore(); rollbackErr != nil {
|
|
||||||
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
nginxUpgradeSnapshot.Cleanup()
|
|
||||||
nginxUpgradeSnapshot = nil
|
|
||||||
if out, rollbackErr := compose.Up(originalInstall.GetComposePath()); rollbackErr != nil {
|
|
||||||
if out != "" {
|
|
||||||
rollbackErr = fmt.Errorf("%s: %w", out, rollbackErr)
|
|
||||||
}
|
|
||||||
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
originalInstall.Status = constant.StatusRunning
|
|
||||||
originalInstall.Message = ""
|
|
||||||
if rollbackErr := appInstallRepo.Save(context.Background(), &originalInstall); rollbackErr != nil {
|
|
||||||
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
install = originalInstall
|
|
||||||
t.LogSuccess(i18n.GetWithName("AppRecover", install.Name))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if install.App.Key == constant.AppOpenresty {
|
|
||||||
if rollbackErr := appInstallRepo.Save(context.Background(), &originalInstall); rollbackErr != nil {
|
|
||||||
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), rollbackErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
install = originalInstall
|
|
||||||
t.LogSuccess(i18n.GetWithName("AppRecover", install.Name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
upgradeTimeout := 1 * time.Hour
|
|
||||||
if install.App.Key == constant.AppOpenresty {
|
|
||||||
// Dynamic modules are built serially and each Docker build has its own
|
|
||||||
// timeout. An outer deadline would start rollback while upgradeApp is
|
|
||||||
// still mutating the installation because SubTask does not stop its
|
|
||||||
// action goroutine on timeout.
|
|
||||||
upgradeTimeout = 0
|
|
||||||
}
|
|
||||||
upgradeTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskUpgrade, task.TaskScopeApp), upgradeApp, rollBackApp, 0, upgradeTimeout)
|
|
||||||
|
|
||||||
upgradingInstall := install
|
|
||||||
if err = appInstallRepo.Save(context.Background(), &upgradingInstall); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
if taskErr := upgradeTask.Execute(); taskErr != nil {
|
|
||||||
existInstall, _ := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
|
|
||||||
if existInstall.ID > 0 && existInstall.Status != constant.StatusRunning {
|
|
||||||
existInstall.Status = constant.StatusUpgradeErr
|
|
||||||
existInstall.Message = taskErr.Error()
|
|
||||||
_ = appInstallRepo.Save(context.Background(), &existInstall)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func skipCheckStatus(service types.ServiceConfig) bool {
|
func skipCheckStatus(service types.ServiceConfig) bool {
|
||||||
for key := range service.Labels {
|
for key := range service.Labels {
|
||||||
if key == "skipStatusCheck" {
|
if key == "skipStatusCheck" {
|
||||||
@@ -1798,7 +1443,9 @@ func synAppInstall(containers map[string]container.Summary, appInstall *model.Ap
|
|||||||
}
|
}
|
||||||
appInstall.Status = constant.StatusError
|
appInstall.Status = constant.StatusError
|
||||||
appInstall.Message = buserr.WithName("ErrContainerNotFound", strings.Join(containerNames, ",")).Error()
|
appInstall.Message = buserr.WithName("ErrContainerNotFound", strings.Join(containerNames, ",")).Error()
|
||||||
_ = appInstallRepo.Save(context.Background(), appInstall)
|
if !global.CONF.Base.IsDemo {
|
||||||
|
_ = appInstallRepo.Save(context.Background(), appInstall)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
notFoundNames := make([]string, 0)
|
notFoundNames := make([]string, 0)
|
||||||
@@ -1857,10 +1504,12 @@ func synAppInstall(containers map[string]container.Summary, appInstall *model.Ap
|
|||||||
appInstall.Message = msg
|
appInstall.Message = msg
|
||||||
appInstall.Status = constant.StatusUnHealthy
|
appInstall.Status = constant.StatusUnHealthy
|
||||||
}
|
}
|
||||||
_ = appInstallRepo.Save(context.Background(), appInstall)
|
if !global.CONF.Base.IsDemo {
|
||||||
|
_ = appInstallRepo.Save(context.Background(), appInstall)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpdate bool) ([]response.AppInstallDTO, error) {
|
func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpdate, readOnly bool) ([]response.AppInstallDTO, error) {
|
||||||
var (
|
var (
|
||||||
res []response.AppInstallDTO
|
res []response.AppInstallDTO
|
||||||
containersMap map[string]container.Summary
|
containersMap map[string]container.Summary
|
||||||
@@ -1891,6 +1540,9 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
|||||||
resourceKeys := getAppInstallResourceKeys(installed.ID)
|
resourceKeys := getAppInstallResourceKeys(installed.ID)
|
||||||
envMap := make(map[string]interface{})
|
envMap := make(map[string]interface{})
|
||||||
_ = json.Unmarshal([]byte(installed.Env), &envMap)
|
_ = json.Unmarshal([]byte(installed.Env), &envMap)
|
||||||
|
if isDemoReadOnly(readOnly) {
|
||||||
|
redactSensitiveValues(envMap)
|
||||||
|
}
|
||||||
installDTO := response.AppInstallDTO{
|
installDTO := response.AppInstallDTO{
|
||||||
ID: installed.ID,
|
ID: installed.ID,
|
||||||
Name: installed.Name,
|
Name: installed.Name,
|
||||||
@@ -1935,7 +1587,9 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
installDTO.DockerCompose = installed.DockerCompose
|
if !isDemoReadOnly(readOnly) {
|
||||||
|
installDTO.DockerCompose = installed.DockerCompose
|
||||||
|
}
|
||||||
installDTO.IsEdit = isEditCompose(installed)
|
installDTO.IsEdit = isEditCompose(installed)
|
||||||
|
|
||||||
details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(installed.App.ID))
|
details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(installed.App.ID))
|
||||||
@@ -2233,6 +1887,10 @@ func isHostModel(dockerCompose string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error {
|
func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error {
|
||||||
|
return copyAppDetailMissingTracked(fileOp, srcDir, dstDir, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyAppDetailMissingTracked(fileOp files.FileOp, srcDir, dstDir string, createdPaths *[]string) error {
|
||||||
entries, err := os.ReadDir(srcDir)
|
entries, err := os.ReadDir(srcDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -2244,6 +1902,9 @@ func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error {
|
|||||||
srcPath := path.Join(srcDir, entry.Name())
|
srcPath := path.Join(srcDir, entry.Name())
|
||||||
dstPath := path.Join(dstDir, entry.Name())
|
dstPath := path.Join(dstDir, entry.Name())
|
||||||
if !fileOp.Stat(dstPath) {
|
if !fileOp.Stat(dstPath) {
|
||||||
|
if createdPaths != nil {
|
||||||
|
*createdPaths = append(*createdPaths, dstPath)
|
||||||
|
}
|
||||||
if entry.IsDir() {
|
if entry.IsDir() {
|
||||||
if err := fileOp.CopyDir(srcPath, dstDir); err != nil {
|
if err := fileOp.CopyDir(srcPath, dstDir); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -2258,7 +1919,7 @@ func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error {
|
|||||||
if !entry.IsDir() {
|
if !entry.IsDir() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := copyAppDetailMissing(fileOp, srcPath, dstPath); err != nil {
|
if err := copyAppDetailMissingTracked(fileOp, srcPath, dstPath, createdPaths); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ type IBackupService interface {
|
|||||||
CheckUsed(name string, isPublic bool) error
|
CheckUsed(name string, isPublic bool) error
|
||||||
|
|
||||||
LoadBackupOptions() ([]dto.BackupOption, error)
|
LoadBackupOptions() ([]dto.BackupOption, error)
|
||||||
SearchWithPage(search dto.SearchPageWithType) (int64, interface{}, error)
|
SearchWithPage(search dto.SearchPageWithType, readOnly ...bool) (int64, interface{}, error)
|
||||||
Create(backupDto dto.BackupOperate) error
|
Create(backupDto dto.BackupOperate) error
|
||||||
CheckConn(req dto.BackupOperate) dto.BackupCheckRes
|
CheckConn(req dto.BackupOperate) dto.BackupCheckRes
|
||||||
GetBuckets(backupDto dto.ForBuckets) ([]interface{}, error)
|
GetBuckets(backupDto dto.ForBuckets) ([]interface{}, error)
|
||||||
@@ -80,7 +80,7 @@ func (u *BackupService) GetLocalDir() (string, error) {
|
|||||||
return account.BackupPath, nil
|
return account.BackupPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *BackupService) SearchWithPage(req dto.SearchPageWithType) (int64, interface{}, error) {
|
func (u *BackupService) SearchWithPage(req dto.SearchPageWithType, readOnly ...bool) (int64, interface{}, error) {
|
||||||
options := []repo.DBOption{repo.WithOrderDesc("created_at")}
|
options := []repo.DBOption{repo.WithOrderDesc("created_at")}
|
||||||
if len(req.Type) != 0 {
|
if len(req.Type) != 0 {
|
||||||
options = append(options, repo.WithByType(req.Type))
|
options = append(options, repo.WithByType(req.Type))
|
||||||
@@ -128,11 +128,36 @@ func (u *BackupService) SearchWithPage(req dto.SearchPageWithType) (int64, inter
|
|||||||
itemVars, _ := json.Marshal(varMap)
|
itemVars, _ := json.Marshal(varMap)
|
||||||
item.Vars = string(itemVars)
|
item.Vars = string(itemVars)
|
||||||
}
|
}
|
||||||
|
if isDemoReadOnly(readOnly...) {
|
||||||
|
item.AccessKey = ""
|
||||||
|
item.Credential = ""
|
||||||
|
item.Vars = sanitizeBackupVars(item.Vars)
|
||||||
|
}
|
||||||
data = append(data, item)
|
data = append(data, item)
|
||||||
}
|
}
|
||||||
return count, data, nil
|
return count, data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sanitizeBackupVars(vars string) string {
|
||||||
|
if vars == "" {
|
||||||
|
return vars
|
||||||
|
}
|
||||||
|
var values map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(vars), &values); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for key := range values {
|
||||||
|
if isSensitiveConfigKey(key) || normalizeConfigKey(key) == "code" {
|
||||||
|
delete(values, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(values)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
func (u *BackupService) CheckConn(req dto.BackupOperate) dto.BackupCheckRes {
|
func (u *BackupService) CheckConn(req dto.BackupOperate) dto.BackupCheckRes {
|
||||||
var res dto.BackupCheckRes
|
var res dto.BackupCheckRes
|
||||||
var backup model.BackupAccount
|
var backup model.BackupAccount
|
||||||
|
|||||||
@@ -90,6 +90,34 @@ func (u *BackupService) AppBackup(req dto.CommonBackup) (*model.BackupRecord, er
|
|||||||
return record, nil
|
return record, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func backupAppWithParentTask(install *model.AppInstall, parentTask *task.Task, fileName string) (*model.BackupRecord, error) {
|
||||||
|
itemDir := fmt.Sprintf("app/%s/%s", install.App.Key, install.Name)
|
||||||
|
backupDir := path.Join(global.Dir.LocalBackupDir, itemDir)
|
||||||
|
record := &model.BackupRecord{
|
||||||
|
Type: "app",
|
||||||
|
Name: install.App.Key,
|
||||||
|
DetailName: install.Name,
|
||||||
|
SourceAccountIDs: "1",
|
||||||
|
DownloadAccountID: 1,
|
||||||
|
FileDir: itemDir,
|
||||||
|
FileName: fileName,
|
||||||
|
TaskID: parentTask.TaskID,
|
||||||
|
Status: constant.StatusWaiting,
|
||||||
|
}
|
||||||
|
if err := backupRepo.CreateRecord(record); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := handleAppBackup(install, parentTask, record.ID, backupDir, fileName, "", "", parentTask.TaskID); err != nil {
|
||||||
|
markBackupFailed(record.ID, err)
|
||||||
|
record.Status = constant.StatusFailed
|
||||||
|
record.Message = err.Error()
|
||||||
|
return record, err
|
||||||
|
}
|
||||||
|
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||||
|
record.Status = constant.StatusSuccess
|
||||||
|
return record, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (u *BackupService) AppRecover(req dto.CommonRecover) error {
|
func (u *BackupService) AppRecover(req dto.CommonRecover) error {
|
||||||
app, err := appRepo.GetFirst(appRepo.WithKey(req.Name))
|
app, err := appRepo.GetFirst(appRepo.WithKey(req.Name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -203,7 +231,11 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
_, _ = compose.Up(install.GetComposePath())
|
if isRollback {
|
||||||
|
_, _ = compose.UpWithoutBuild(install.GetComposePath())
|
||||||
|
} else {
|
||||||
|
_, _ = compose.Up(install.GetComposePath())
|
||||||
|
}
|
||||||
_ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", ""))
|
_ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", ""))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@@ -39,6 +40,7 @@ func (u *BackupService) MysqlBackup(req dto.CommonBackup) error {
|
|||||||
TaskID: req.TaskID,
|
TaskID: req.TaskID,
|
||||||
Status: constant.StatusWaiting,
|
Status: constant.StatusWaiting,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
|
Args: encodeBackupArgs(req.Args),
|
||||||
}
|
}
|
||||||
if err := backupRepo.CreateRecord(record); err != nil {
|
if err := backupRepo.CreateRecord(record); err != nil {
|
||||||
global.LOG.Errorf("save backup record failed, err: %v", err)
|
global.LOG.Errorf("save backup record failed, err: %v", err)
|
||||||
@@ -143,6 +145,14 @@ func handleMysqlRecover(req dto.CommonRecover, parentTask *task.Task, isRollback
|
|||||||
|
|
||||||
if !isRollback {
|
if !isRollback {
|
||||||
rollbackFile := path.Join(global.Dir.TmpDir, fmt.Sprintf("database/%s/%s_%s.sql.gz", req.Type, req.DetailName, time.Now().Format(constant.DateTimeSlimLayout)))
|
rollbackFile := path.Join(global.Dir.TmpDir, fmt.Sprintf("database/%s/%s_%s.sql.gz", req.Type, req.DetailName, time.Now().Format(constant.DateTimeSlimLayout)))
|
||||||
|
var rollbackArgs []string
|
||||||
|
if req.BackupRecordID != 0 {
|
||||||
|
record, err := backupRepo.GetRecord(repo.WithByID(req.BackupRecordID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rollbackArgs = decodeBackupArgs(record.Args)
|
||||||
|
}
|
||||||
if err := cli.Backup(client.BackupInfo{
|
if err := cli.Backup(client.BackupInfo{
|
||||||
Name: req.DetailName,
|
Name: req.DetailName,
|
||||||
Type: req.Type,
|
Type: req.Type,
|
||||||
@@ -150,6 +160,7 @@ func handleMysqlRecover(req dto.CommonRecover, parentTask *task.Task, isRollback
|
|||||||
Format: dbInfo.Format,
|
Format: dbInfo.Format,
|
||||||
TargetDir: path.Dir(rollbackFile),
|
TargetDir: path.Dir(rollbackFile),
|
||||||
FileName: path.Base(rollbackFile),
|
FileName: path.Base(rollbackFile),
|
||||||
|
Args: rollbackArgs,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("backup mysql db %s for rollback before recover failed, err: %v", req.DetailName, err)
|
return fmt.Errorf("backup mysql db %s for rollback before recover failed, err: %v", req.DetailName, err)
|
||||||
}
|
}
|
||||||
@@ -242,6 +253,36 @@ func doMysqlBackup(db DatabaseHelper, targetDir, fileName, secret string) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func encodeBackupArgs(args []string) string {
|
||||||
|
var items []string
|
||||||
|
for _, arg := range args {
|
||||||
|
if len(arg) != 0 {
|
||||||
|
items = append(items, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(items) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(items)
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Warnf("marshal backup args failed: %v", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeBackupArgs(value string) []string {
|
||||||
|
if len(value) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var args []string
|
||||||
|
if err := json.Unmarshal([]byte(value), &args); err != nil {
|
||||||
|
global.LOG.Warnf("unmarshal backup args failed: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
func loadSqlFile(file string) (string, error) {
|
func loadSqlFile(file string) (string, error) {
|
||||||
if !strings.HasSuffix(file, ".tar.gz") && !strings.HasSuffix(file, ".zip") {
|
if !strings.HasSuffix(file, ".tar.gz") && !strings.HasSuffix(file, ".zip") {
|
||||||
return file, nil
|
return file, nil
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ type ClamService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type IClamService interface {
|
type IClamService interface {
|
||||||
LoadBaseInfo() (dto.ClamBaseInfo, error)
|
LoadBaseInfo(readOnly bool) (dto.ClamBaseInfo, error)
|
||||||
Operate(operate string) error
|
Operate(operate string) error
|
||||||
SearchWithPage(search dto.SearchClamWithPage) (int64, interface{}, error)
|
SearchWithPage(search dto.SearchClamWithPage) (int64, interface{}, error)
|
||||||
Create(req dto.ClamCreate, operator string) error
|
Create(req dto.ClamCreate, operator string) error
|
||||||
@@ -53,7 +53,7 @@ func NewIClamService() IClamService {
|
|||||||
return &ClamService{}
|
return &ClamService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
|
func (c *ClamService) LoadBaseInfo(readOnly bool) (dto.ClamBaseInfo, error) {
|
||||||
var baseInfo dto.ClamBaseInfo
|
var baseInfo dto.ClamBaseInfo
|
||||||
baseInfo.Version = "-"
|
baseInfo.Version = "-"
|
||||||
baseInfo.FreshVersion = "-"
|
baseInfo.FreshVersion = "-"
|
||||||
@@ -96,7 +96,7 @@ func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
|
|||||||
baseInfo.Version = strings.TrimPrefix(version, "ClamAV ")
|
baseInfo.Version = strings.TrimPrefix(version, "ClamAV ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if !readOnly && !global.CONF.Base.IsDemo {
|
||||||
_ = clam.CheckWithStopAll(false, clamRepo)
|
_ = clam.CheckWithStopAll(false, clamRepo)
|
||||||
}
|
}
|
||||||
if baseInfo.FreshIsActive {
|
if baseInfo.FreshIsActive {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import (
|
|||||||
type ComposeTemplateService struct{}
|
type ComposeTemplateService struct{}
|
||||||
|
|
||||||
type IComposeTemplateService interface {
|
type IComposeTemplateService interface {
|
||||||
List() ([]dto.ComposeTemplateInfo, error)
|
List(readOnly ...bool) ([]dto.ComposeTemplateInfo, error)
|
||||||
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error)
|
SearchWithPage(search dto.SearchWithPage, readOnly ...bool) (int64, interface{}, error)
|
||||||
Create(req dto.ComposeTemplateCreate) error
|
Create(req dto.ComposeTemplateCreate) error
|
||||||
Update(id uint, upMap map[string]interface{}) error
|
Update(id uint, upMap map[string]interface{}) error
|
||||||
Batch(req dto.ComposeTemplateBatch) error
|
Batch(req dto.ComposeTemplateBatch) error
|
||||||
@@ -23,7 +23,7 @@ func NewIComposeTemplateService() IComposeTemplateService {
|
|||||||
return &ComposeTemplateService{}
|
return &ComposeTemplateService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ComposeTemplateService) List() ([]dto.ComposeTemplateInfo, error) {
|
func (u *ComposeTemplateService) List(readOnly ...bool) ([]dto.ComposeTemplateInfo, error) {
|
||||||
composes, err := composeRepo.List()
|
composes, err := composeRepo.List()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, buserr.New("ErrRecordNotFound")
|
return nil, buserr.New("ErrRecordNotFound")
|
||||||
@@ -34,12 +34,15 @@ func (u *ComposeTemplateService) List() ([]dto.ComposeTemplateInfo, error) {
|
|||||||
if err := copier.Copy(&item, &compose); err != nil {
|
if err := copier.Copy(&item, &compose); err != nil {
|
||||||
return nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
return nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||||
}
|
}
|
||||||
|
if isDemoReadOnly(readOnly...) {
|
||||||
|
item.Content = ""
|
||||||
|
}
|
||||||
dtoLists = append(dtoLists, item)
|
dtoLists = append(dtoLists, item)
|
||||||
}
|
}
|
||||||
return dtoLists, err
|
return dtoLists, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ComposeTemplateService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) {
|
func (u *ComposeTemplateService) SearchWithPage(req dto.SearchWithPage, readOnly ...bool) (int64, interface{}, error) {
|
||||||
total, composes, err := composeRepo.Page(req.Page, req.PageSize, repo.WithByLikeName(req.Info))
|
total, composes, err := composeRepo.Page(req.Page, req.PageSize, repo.WithByLikeName(req.Info))
|
||||||
var dtoComposeTemplates []dto.ComposeTemplateInfo
|
var dtoComposeTemplates []dto.ComposeTemplateInfo
|
||||||
for _, compose := range composes {
|
for _, compose := range composes {
|
||||||
@@ -47,6 +50,9 @@ func (u *ComposeTemplateService) SearchWithPage(req dto.SearchWithPage) (int64,
|
|||||||
if err := copier.Copy(&item, &compose); err != nil {
|
if err := copier.Copy(&item, &compose); err != nil {
|
||||||
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||||
}
|
}
|
||||||
|
if isDemoReadOnly(readOnly...) {
|
||||||
|
item.Content = ""
|
||||||
|
}
|
||||||
dtoComposeTemplates = append(dtoComposeTemplates, item)
|
dtoComposeTemplates = append(dtoComposeTemplates, item)
|
||||||
}
|
}
|
||||||
return total, dtoComposeTemplates, err
|
return total, dtoComposeTemplates, err
|
||||||
|
|||||||
@@ -65,18 +65,19 @@ type IContainerService interface {
|
|||||||
PageVolume(req dto.SearchWithPage) (int64, interface{}, error)
|
PageVolume(req dto.SearchWithPage) (int64, interface{}, error)
|
||||||
ListVolume() ([]dto.Options, error)
|
ListVolume() ([]dto.Options, error)
|
||||||
|
|
||||||
PageCompose(req dto.SearchWithPage) (int64, interface{}, error)
|
PageCompose(req dto.SearchWithPage, readOnly ...bool) (int64, interface{}, error)
|
||||||
LoadComposeEnv(name string) (string, error)
|
LoadComposeEnv(name string) (string, error)
|
||||||
CreateCompose(req dto.ComposeCreate) error
|
CreateCompose(req dto.ComposeCreate) error
|
||||||
ComposeOperation(req dto.ComposeOperation) error
|
ComposeOperation(req dto.ComposeOperation) error
|
||||||
TestCompose(req dto.ComposeCreate) (bool, error)
|
TestCompose(req dto.ComposeCreate) (bool, error)
|
||||||
ComposeUpdate(req dto.ComposeUpdate) error
|
ComposeUpdate(req dto.ComposeUpdate) error
|
||||||
|
ComposePin(req dto.ComposePin) error
|
||||||
ComposeLogClean(req dto.ComposeLogClean) error
|
ComposeLogClean(req dto.ComposeLogClean) error
|
||||||
|
|
||||||
ContainerCreate(req dto.ContainerOperate, inThread bool) error
|
ContainerCreate(req dto.ContainerOperate, inThread bool) error
|
||||||
ContainerUpdate(req dto.ContainerOperate) error
|
ContainerUpdate(req dto.ContainerOperate) error
|
||||||
ContainerUpgrade(req dto.ContainerUpgrade) error
|
ContainerUpgrade(req dto.ContainerUpgrade) error
|
||||||
ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error)
|
ContainerInfo(req dto.OperationWithName, readOnly ...bool) (*dto.ContainerOperate, error)
|
||||||
ContainerListStats() ([]dto.ContainerListStats, error)
|
ContainerListStats() ([]dto.ContainerListStats, error)
|
||||||
ContainerItemStats(req dto.OperationWithName) (dto.ContainerItemStats, error)
|
ContainerItemStats(req dto.OperationWithName) (dto.ContainerItemStats, error)
|
||||||
LoadResourceLimit() (*dto.ResourceLimit, error)
|
LoadResourceLimit() (*dto.ResourceLimit, error)
|
||||||
@@ -567,7 +568,7 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
|
|||||||
return taskItem.Execute()
|
return taskItem.Execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error) {
|
func (u *ContainerService) ContainerInfo(req dto.OperationWithName, readOnly ...bool) (*dto.ContainerOperate, error) {
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -611,6 +612,9 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai
|
|||||||
data.Tty = oldContainer.Config.Tty
|
data.Tty = oldContainer.Config.Tty
|
||||||
data.Entrypoint = oldContainer.Config.Entrypoint
|
data.Entrypoint = oldContainer.Config.Entrypoint
|
||||||
data.Env = oldContainer.Config.Env
|
data.Env = oldContainer.Config.Env
|
||||||
|
if isDemoReadOnly(readOnly...) {
|
||||||
|
data.Env = nil
|
||||||
|
}
|
||||||
data.CPUShares = oldContainer.HostConfig.CPUShares
|
data.CPUShares = oldContainer.HostConfig.CPUShares
|
||||||
for key, val := range oldContainer.Config.Labels {
|
for key, val := range oldContainer.Config.Labels {
|
||||||
data.Labels = append(data.Labels, fmt.Sprintf("%s=%s", key, val))
|
data.Labels = append(data.Labels, fmt.Sprintf("%s=%s", key, val))
|
||||||
@@ -2025,6 +2029,9 @@ func loadComposeCount(client *client.Client) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, compose := range composeCreatedByLocal {
|
for _, compose := range composeCreatedByLocal {
|
||||||
|
if len(compose.Path) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if _, has := composeMap[compose.Name]; !has {
|
if _, has := composeMap[compose.Name]; !has {
|
||||||
composeMap[compose.Name] = struct{}{}
|
composeMap[compose.Name] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -23,8 +24,10 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/filters"
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const composeProjectLabel = "com.docker.compose.project"
|
const composeProjectLabel = "com.docker.compose.project"
|
||||||
@@ -32,7 +35,7 @@ const composeConfigLabel = "com.docker.compose.project.config_files"
|
|||||||
const composeWorkdirLabel = "com.docker.compose.project.working_dir"
|
const composeWorkdirLabel = "com.docker.compose.project.working_dir"
|
||||||
const composeCreatedBy = "createdBy"
|
const composeCreatedBy = "createdBy"
|
||||||
|
|
||||||
func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface{}, error) {
|
func (u *ContainerService) PageCompose(req dto.SearchWithPage, readOnly ...bool) (int64, interface{}, error) {
|
||||||
var (
|
var (
|
||||||
records []dto.ComposeInfo
|
records []dto.ComposeInfo
|
||||||
BackDatas []dto.ComposeInfo
|
BackDatas []dto.ComposeInfo
|
||||||
@@ -52,7 +55,15 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
composeCreatedByLocal, _ := composeRepo.ListRecord()
|
composeRecords, _ := composeRepo.ListRecord()
|
||||||
|
pinnedByName := make(map[string]bool, len(composeRecords))
|
||||||
|
composeCreatedByLocal := make([]model.Compose, 0, len(composeRecords))
|
||||||
|
for _, record := range composeRecords {
|
||||||
|
pinnedByName[record.Name] = record.IsPinned
|
||||||
|
if len(record.Path) != 0 {
|
||||||
|
composeCreatedByLocal = append(composeCreatedByLocal, record)
|
||||||
|
}
|
||||||
|
}
|
||||||
composeLocalMap := make(map[string]dto.ComposeInfo)
|
composeLocalMap := make(map[string]dto.ComposeInfo)
|
||||||
for _, localItem := range composeCreatedByLocal {
|
for _, localItem := range composeCreatedByLocal {
|
||||||
composeItemLocal := dto.ComposeInfo{
|
composeItemLocal := dto.ComposeInfo{
|
||||||
@@ -136,6 +147,7 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
for key, value := range mergedMap {
|
for key, value := range mergedMap {
|
||||||
value.Name = key
|
value.Name = key
|
||||||
value.ComposeFileExists = composeFileExists(value.Workdir, value.ConfigFile)
|
value.ComposeFileExists = composeFileExists(value.Workdir, value.ConfigFile)
|
||||||
|
value.IsPinned = pinnedByName[key]
|
||||||
records = append(records, value)
|
records = append(records, value)
|
||||||
}
|
}
|
||||||
if len(req.Info) != 0 {
|
if len(req.Info) != 0 {
|
||||||
@@ -149,7 +161,21 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if req.ExcludeAppStore {
|
||||||
|
length, count := len(records), 0
|
||||||
|
for count < length {
|
||||||
|
if records[count].CreatedBy == "Apps" {
|
||||||
|
records = append(records[:count], records[(count+1):]...)
|
||||||
|
length--
|
||||||
|
} else {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
sort.Slice(records, func(i, j int) bool {
|
sort.Slice(records, func(i, j int) bool {
|
||||||
|
if records[i].IsPinned != records[j].IsPinned {
|
||||||
|
return records[i].IsPinned
|
||||||
|
}
|
||||||
return records[i].CreatedAt > records[j].CreatedAt
|
return records[i].CreatedAt > records[j].CreatedAt
|
||||||
})
|
})
|
||||||
total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
||||||
@@ -161,7 +187,10 @@ func (u *ContainerService) PageCompose(req dto.SearchWithPage) (int64, interface
|
|||||||
}
|
}
|
||||||
BackDatas = records[start:end]
|
BackDatas = records[start:end]
|
||||||
}
|
}
|
||||||
listItem := loadEnv(BackDatas)
|
listItem := BackDatas
|
||||||
|
if !isDemoReadOnly(readOnly...) {
|
||||||
|
listItem = loadEnv(BackDatas)
|
||||||
|
}
|
||||||
return int64(total), listItem, nil
|
return int64(total), listItem, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,53 +218,62 @@ func composeFileExists(workdir, configFile string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
|
func (u *ContainerService) TestCompose(req dto.ComposeCreate) (bool, error) {
|
||||||
if cmd.CheckIllegal(req.Path) {
|
if err := validateComposeCreateName(req); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if hasIllegalComposeCreateInput(req) {
|
||||||
return false, buserr.New("ErrCmdIllegal")
|
return false, buserr.New("ErrCmdIllegal")
|
||||||
}
|
}
|
||||||
composeItem, _ := composeRepo.GetRecord(repo.WithByName(req.Name))
|
projectName, err := resolveComposeCreateProjectName(req)
|
||||||
if composeItem.ID != 0 {
|
|
||||||
return false, buserr.New("ErrRecordExist")
|
|
||||||
}
|
|
||||||
if err := u.loadPath(&req); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if err := newComposeEnv(req.Path, req.Env); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
cmd := getComposeCmd(req.Path, "config")
|
|
||||||
stdout, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("docker-compose config failed, std: %s, err: %v", string(stdout), err)
|
return false, err
|
||||||
|
}
|
||||||
|
if err := checkComposeCreateDuplicate(req, projectName); err != nil {
|
||||||
|
return false, err
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
|
func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
|
||||||
if cmd.CheckIllegal(req.Name, req.Path) {
|
if err := validateComposeCreateName(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if hasIllegalComposeCreateInput(req) {
|
||||||
return buserr.New("ErrCmdIllegal")
|
return buserr.New("ErrCmdIllegal")
|
||||||
}
|
}
|
||||||
|
projectName, err := resolveComposeCreateProjectName(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := checkComposeCreateDuplicate(req, projectName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := u.loadPath(&req); err != nil {
|
if err := u.loadPath(&req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if req.From == "path" {
|
if err := newComposeEnv(req.Path, req.Env); err != nil {
|
||||||
req.Name = path.Base(path.Dir(req.Path))
|
return err
|
||||||
}
|
}
|
||||||
|
req.Name = projectName
|
||||||
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeCompose, req.TaskID, 1)
|
taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeCompose, req.TaskID, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("new task for image build failed, err: %v", err)
|
return fmt.Errorf("new task for image build failed, err: %v", err)
|
||||||
}
|
}
|
||||||
if err := newComposeEnv(req.Path, req.Env); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
go func() {
|
go func() {
|
||||||
taskItem.AddSubTask(i18n.GetMsgByKey("ComposeCreate"), func(t *task.Task) error {
|
taskItem.AddSubTask(i18n.GetMsgByKey("ComposeCreate"), func(t *task.Task) error {
|
||||||
err := compose.UpWithTask(req.Path, t, req.ForcePull)
|
err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name)
|
||||||
t.LogWithStatus(i18n.GetMsgByKey("ComposeCreate"), err)
|
t.LogWithStatus(i18n.GetMsgByKey("ComposeCreate"), err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = compose.Down(req.Path)
|
_, _ = compose.Down(req.Path, req.Name)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_ = composeRepo.CreateRecord(&model.Compose{Name: strings.ToLower(req.Name), Path: req.Path})
|
recordName := strings.ToLower(req.Name)
|
||||||
|
record, _ := composeRepo.GetRecord(repo.WithByName(recordName))
|
||||||
|
if record.ID == 0 {
|
||||||
|
_ = composeRepo.CreateRecord(&model.Compose{Name: recordName, Path: req.Path})
|
||||||
|
} else {
|
||||||
|
_ = composeRepo.UpdateRecord(recordName, map[string]interface{}{"path": req.Path})
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}, nil)
|
}, nil)
|
||||||
_ = taskItem.Execute()
|
_ = taskItem.Execute()
|
||||||
@@ -244,6 +282,243 @@ func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkComposeRecordName(name string) error {
|
||||||
|
composeItem, _ := composeRepo.GetRecord(repo.WithByName(name))
|
||||||
|
if composeItem.ID != 0 && len(composeItem.Path) != 0 {
|
||||||
|
return buserr.New("ErrRecordExist")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkComposeCreateDuplicate(req dto.ComposeCreate, projectName string) error {
|
||||||
|
if err := checkComposeRecordName(projectName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if req.From == "path" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
composeItem, _ := composeRepo.GetRecord(repo.WithByPath(composeCreatePath(req)))
|
||||||
|
if composeItem.ID != 0 && composeItem.Path != "" {
|
||||||
|
return buserr.New("ErrRecordExist")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateComposeCreateName(req dto.ComposeCreate) error {
|
||||||
|
if req.From == "path" {
|
||||||
|
name := strings.TrimSpace(req.Name)
|
||||||
|
if name != "" && !re.GetRegex(re.ComposeNamePattern).MatchString(name) {
|
||||||
|
return buserr.New("ErrComposeNameInvalid")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !re.GetRegex(re.ComposeNamePattern).MatchString(composeCreateDirName(req)) {
|
||||||
|
return buserr.New("ErrComposeNameInvalid")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasIllegalComposeCreateInput(req dto.ComposeCreate) bool {
|
||||||
|
if req.From == "path" {
|
||||||
|
return cmd.CheckIllegal(req.Name, req.Path)
|
||||||
|
}
|
||||||
|
return cmd.CheckIllegal(composeCreateDirName(req))
|
||||||
|
}
|
||||||
|
|
||||||
|
func composeCreateDirName(req dto.ComposeCreate) string {
|
||||||
|
dirName := strings.TrimSpace(req.DirName)
|
||||||
|
if dirName == "" {
|
||||||
|
// Keep compatibility with callers that used name as both the directory and
|
||||||
|
// Compose project name before dirName was introduced.
|
||||||
|
return strings.TrimSpace(req.Name)
|
||||||
|
}
|
||||||
|
return dirName
|
||||||
|
}
|
||||||
|
|
||||||
|
func composeCreatePath(req dto.ComposeCreate) string {
|
||||||
|
return filepath.Join(global.Dir.DataDir, "docker", "compose", composeCreateDirName(req), "docker-compose.yml")
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveComposeCreateProjectName(req dto.ComposeCreate) (string, error) {
|
||||||
|
if req.From == "path" {
|
||||||
|
envPath, err := createComposeTempFile(
|
||||||
|
filepath.Dir(primaryComposePath(req.Path)),
|
||||||
|
".1panel-compose-*.env",
|
||||||
|
req.Env,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer os.Remove(envPath)
|
||||||
|
return resolveComposeProjectName(req.Path, req.Name, envPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := filepath.Dir(composeCreatePath(req))
|
||||||
|
cleanupDir, err := prepareComposeStagingDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer cleanupDir()
|
||||||
|
|
||||||
|
composePath, err := createComposeTempFile(dir, ".1panel-compose-*.yml", req.File)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer os.Remove(composePath)
|
||||||
|
|
||||||
|
envPath, err := createComposeTempFile(dir, ".1panel-compose-*.env", req.Env)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer os.Remove(envPath)
|
||||||
|
|
||||||
|
return resolveComposeProjectName(composePath, "", envPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createComposeTempFile(dir, pattern, content string) (string, error) {
|
||||||
|
file, err := os.CreateTemp(dir, pattern)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
filePath := file.Name()
|
||||||
|
if _, err := file.WriteString(content); err != nil {
|
||||||
|
_ = file.Close()
|
||||||
|
_ = os.Remove(filePath)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
_ = os.Remove(filePath)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareComposeStagingDir(dir string) (func(), error) {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dir), os.ModePerm); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
created := false
|
||||||
|
if err := os.Mkdir(dir, os.ModePerm); err != nil {
|
||||||
|
if !errors.Is(err, os.ErrExist) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
created = true
|
||||||
|
}
|
||||||
|
return func() {
|
||||||
|
if created {
|
||||||
|
_ = os.Remove(dir)
|
||||||
|
}
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveComposeProjectName(composePath, fallbackName, envFile string) (string, error) {
|
||||||
|
// Preserve the name resolved by Compose (including a top-level name) so the
|
||||||
|
// container label and the local record always use the same project identity.
|
||||||
|
parentName := normalizeComposeProjectName(path.Base(path.Dir(primaryComposePath(composePath))))
|
||||||
|
fallbackName = strings.TrimSpace(fallbackName)
|
||||||
|
stdout, err := runComposeConfig(composePath, "", envFile)
|
||||||
|
if err == nil {
|
||||||
|
projectName, parseErr := loadComposeProjectName(stdout)
|
||||||
|
if parseErr != nil {
|
||||||
|
return "", parseErr
|
||||||
|
}
|
||||||
|
if projectName != "" {
|
||||||
|
if !re.GetRegex(re.ComposeNamePattern).MatchString(projectName) {
|
||||||
|
return "", buserr.New("ErrComposeNameInvalid")
|
||||||
|
}
|
||||||
|
return projectName, nil
|
||||||
|
}
|
||||||
|
if parentName != "" {
|
||||||
|
return parentName, nil
|
||||||
|
}
|
||||||
|
if fallbackName != "" {
|
||||||
|
if _, fallbackErr := runComposeConfig(composePath, fallbackName, envFile); fallbackErr != nil {
|
||||||
|
return "", fallbackErr
|
||||||
|
}
|
||||||
|
return fallbackName, nil
|
||||||
|
}
|
||||||
|
return "", buserr.New("ErrComposeProjectNameEmpty")
|
||||||
|
}
|
||||||
|
if !isComposeProjectNameEmptyError(err) {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveErr := err
|
||||||
|
if parentName != "" {
|
||||||
|
if _, parentErr := runComposeConfig(composePath, parentName, envFile); parentErr == nil {
|
||||||
|
return parentName, nil
|
||||||
|
} else {
|
||||||
|
resolveErr = parentErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fallbackName != "" && fallbackName != parentName {
|
||||||
|
if _, fallbackErr := runComposeConfig(composePath, fallbackName, envFile); fallbackErr == nil {
|
||||||
|
return fallbackName, nil
|
||||||
|
} else {
|
||||||
|
return "", fallbackErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if parentName == "" && fallbackName == "" {
|
||||||
|
return "", buserr.New("ErrComposeProjectNameEmpty")
|
||||||
|
}
|
||||||
|
return "", resolveErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func runComposeConfig(composePath, projectName, envFile string) ([]byte, error) {
|
||||||
|
configCmd := getComposeCmdWithEnv(composePath, "config", envFile, projectName)
|
||||||
|
stdout, err := configCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
var stderr []byte
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
stderr = exitErr.Stderr
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("docker-compose config failed, std: %s, err: %v", mergeComposeOutput(stdout, stderr), err)
|
||||||
|
}
|
||||||
|
return stdout, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeComposeOutput(stdout, stderr []byte) string {
|
||||||
|
outputs := make([]string, 0, 2)
|
||||||
|
if output := strings.TrimSpace(string(stdout)); output != "" {
|
||||||
|
outputs = append(outputs, output)
|
||||||
|
}
|
||||||
|
if output := strings.TrimSpace(string(stderr)); output != "" {
|
||||||
|
outputs = append(outputs, output)
|
||||||
|
}
|
||||||
|
return strings.Join(outputs, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadComposeProjectName(config []byte) (string, error) {
|
||||||
|
var project struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(config, &project); err != nil {
|
||||||
|
return "", buserr.WithDetail("ErrComposeProjectNameParse", err.Error(), err)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(project.Name), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func primaryComposePath(composePath string) string {
|
||||||
|
if index := strings.Index(composePath, ","); index >= 0 {
|
||||||
|
return composePath[:index]
|
||||||
|
}
|
||||||
|
return composePath
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeComposeProjectName(name string) string {
|
||||||
|
name = re.GetRegex(re.ComposeDisallowedCharsPattern).
|
||||||
|
ReplaceAllString(strings.ToLower(strings.TrimSpace(name)), "")
|
||||||
|
return strings.TrimLeft(name, "_-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isComposeProjectNameEmptyError(err error) bool {
|
||||||
|
message := strings.ToLower(err.Error())
|
||||||
|
return strings.Contains(message, "project name must not be empty") ||
|
||||||
|
strings.Contains(message, "project name can't be empty")
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
||||||
if len(req.Path) == 0 && req.Operation == "delete" {
|
if len(req.Path) == 0 && req.Operation == "delete" {
|
||||||
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
_ = composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
||||||
@@ -267,15 +542,15 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if req.Operation == "up" {
|
if req.Operation == "up" {
|
||||||
if stdout, err := compose.Up(req.Path); err != nil {
|
if stdout, err := compose.Up(req.Path, req.Name); err != nil {
|
||||||
return fmt.Errorf("docker-compose up failed, std: %s, err: %v", stdout, err)
|
return fmt.Errorf("docker-compose up failed, std: %s, err: %v", stdout, err)
|
||||||
}
|
}
|
||||||
} else if req.Operation == "rebuild" {
|
} else if req.Operation == "rebuild" {
|
||||||
if stdout, err := compose.DownAndUp(req.Path); err != nil {
|
if stdout, err := compose.DownAndUp(req.Path, req.Name); err != nil {
|
||||||
return fmt.Errorf("docker-compose rebuild failed, std: %s, err: %v", stdout, err)
|
return fmt.Errorf("docker-compose rebuild failed, std: %s, err: %v", stdout, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if stdout, err := compose.Operate(req.Path, req.Operation); err != nil {
|
if stdout, err := compose.Operate(req.Path, req.Operation, req.Name); err != nil {
|
||||||
return fmt.Errorf("docker-compose %s failed, std: %s, err: %v", req.Operation, stdout, err)
|
return fmt.Errorf("docker-compose %s failed, std: %s, err: %v", req.Operation, stdout, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,9 +586,9 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := compose.UpWithTask(req.Path, t, req.ForcePull); err != nil {
|
if err := compose.UpWithTask(req.Path, t, req.ForcePull, req.Name); err != nil {
|
||||||
global.LOG.Errorf("update failed when handle compose up, err: %s, now try to recreate the old compose file", err)
|
global.LOG.Errorf("update failed when handle compose up, err: %s, now try to recreate the old compose file", err)
|
||||||
if err := recreateCompose(string(oldFile), req.Path); err != nil {
|
if err := recreateCompose(string(oldFile), req.Path, req.Name); err != nil {
|
||||||
return fmt.Errorf("update failed and recreate old compose file also failed, err: %v", err)
|
return fmt.Errorf("update failed and recreate old compose file also failed, err: %v", err)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("update failed when handle compose up, err: %s", err)
|
return fmt.Errorf("update failed when handle compose up, err: %s", err)
|
||||||
@@ -327,6 +602,20 @@ func (u *ContainerService) ComposeUpdate(req dto.ComposeUpdate) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ContainerService) ComposePin(req dto.ComposePin) error {
|
||||||
|
record, _ := composeRepo.GetRecord(repo.WithByName(req.Name))
|
||||||
|
if record.ID == 0 {
|
||||||
|
if !req.IsPinned {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return composeRepo.CreateRecord(&model.Compose{Name: req.Name, IsPinned: true})
|
||||||
|
}
|
||||||
|
if !req.IsPinned && len(record.Path) == 0 {
|
||||||
|
return composeRepo.DeleteRecord(repo.WithByName(req.Name))
|
||||||
|
}
|
||||||
|
return composeRepo.UpdateRecord(req.Name, map[string]interface{}{"is_pinned": req.IsPinned})
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ContainerService) ComposeLogClean(req dto.ComposeLogClean) error {
|
func (u *ContainerService) ComposeLogClean(req dto.ComposeLogClean) error {
|
||||||
client, err := docker.NewDockerClient()
|
client, err := docker.NewDockerClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -389,15 +678,15 @@ func (u *ContainerService) LoadComposeEnv(name string) (string, error) {
|
|||||||
|
|
||||||
func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
|
func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
|
||||||
if req.From == "template" || req.From == "edit" {
|
if req.From == "template" || req.From == "edit" {
|
||||||
dir := fmt.Sprintf("%s/docker/compose/%s", global.Dir.DataDir, req.Name)
|
composePath := composeCreatePath(*req)
|
||||||
|
dir := filepath.Dir(composePath)
|
||||||
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
|
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
|
||||||
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
|
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
path := fmt.Sprintf("%s/docker-compose.yml", dir)
|
file, err := os.OpenFile(composePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, constant.FilePerm)
|
||||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, constant.FilePerm)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -405,14 +694,14 @@ func (u *ContainerService) loadPath(req *dto.ComposeCreate) error {
|
|||||||
write := bufio.NewWriter(file)
|
write := bufio.NewWriter(file)
|
||||||
_, _ = write.WriteString(string(req.File))
|
_, _ = write.WriteString(string(req.File))
|
||||||
write.Flush()
|
write.Flush()
|
||||||
req.Path = path
|
req.Path = composePath
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeContainerForCompose(composeName, composePath string) error {
|
func removeContainerForCompose(composeName, composePath string) error {
|
||||||
if _, err := os.Stat(composePath); err == nil {
|
if _, err := os.Stat(composePath); err == nil {
|
||||||
if stdout, err := compose.Operate(composePath, "down"); err != nil {
|
if stdout, err := compose.Operate(composePath, "down", composeName); err != nil {
|
||||||
return errors.New(stdout)
|
return errors.New(stdout)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -437,7 +726,7 @@ func removeContainerForCompose(composeName, composePath string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func recreateCompose(content, path string) error {
|
func recreateCompose(content, path, projectName string) error {
|
||||||
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640)
|
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0640)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -447,7 +736,7 @@ func recreateCompose(content, path string) error {
|
|||||||
_, _ = write.WriteString(content)
|
_, _ = write.WriteString(content)
|
||||||
write.Flush()
|
write.Flush()
|
||||||
|
|
||||||
if stdout, err := compose.Up(path); err != nil {
|
if stdout, err := compose.Up(path, projectName); err != nil {
|
||||||
return errors.New(string(stdout))
|
return errors.New(string(stdout))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -170,6 +170,10 @@ type containerSwitchClient interface {
|
|||||||
NetworkDisconnect(context.Context, string, string, bool) error
|
NetworkDisconnect(context.Context, string, string, bool) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type containerInspectClient interface {
|
||||||
|
ContainerInspect(context.Context, string) (container.InspectResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
type containerOperationMutex struct {
|
type containerOperationMutex struct {
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
locks map[string]*containerOperationLockEntry
|
locks map[string]*containerOperationLockEntry
|
||||||
@@ -335,7 +339,7 @@ const (
|
|||||||
containerHealthCheckMaxWait = 10 * time.Minute
|
containerHealthCheckMaxWait = 10 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
func waitContainerReady(ctx context.Context, cli containerSwitchClient, containerID string) error {
|
func waitContainerReady(ctx context.Context, cli containerInspectClient, containerID string) error {
|
||||||
info, err := cli.ContainerInspect(ctx, containerID)
|
info, err := cli.ContainerInspect(ctx, containerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -347,14 +351,15 @@ func waitContainerReady(ctx context.Context, cli containerSwitchClient, containe
|
|||||||
return waitContainerStable(ctx, cli, containerID, info)
|
return waitContainerStable(ctx, cli, containerID, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initialRestartCount := info.RestartCount
|
||||||
timeout := containerHealthCheckTimeout(info.Config)
|
timeout := containerHealthCheckTimeout(info.Config)
|
||||||
deadline := time.NewTimer(timeout)
|
deadline := time.NewTimer(timeout)
|
||||||
ticker := time.NewTicker(time.Second)
|
ticker := time.NewTicker(time.Second)
|
||||||
defer deadline.Stop()
|
defer deadline.Stop()
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for {
|
for {
|
||||||
if info.State.Restarting || info.RestartCount != 0 {
|
if info.State.Restarting || info.RestartCount != initialRestartCount {
|
||||||
return fmt.Errorf("container restarted %d times during startup", info.RestartCount)
|
return fmt.Errorf("container restart count changed from %d to %d during startup", initialRestartCount, info.RestartCount)
|
||||||
}
|
}
|
||||||
if info.State.Health == nil {
|
if info.State.Health == nil {
|
||||||
return fmt.Errorf("container health status is unavailable")
|
return fmt.Errorf("container health status is unavailable")
|
||||||
@@ -382,9 +387,10 @@ func waitContainerReady(ctx context.Context, cli containerSwitchClient, containe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitContainerStable(ctx context.Context, cli containerSwitchClient, containerID string, initial container.InspectResponse) error {
|
func waitContainerStable(ctx context.Context, cli containerInspectClient, containerID string, initial container.InspectResponse) error {
|
||||||
startedAt := initial.State.StartedAt
|
startedAt := initial.State.StartedAt
|
||||||
if err := checkContainerStableState(initial, startedAt); err != nil {
|
restartCount := initial.RestartCount
|
||||||
|
if err := checkContainerStableState(initial, startedAt, restartCount); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
deadline := time.NewTimer(containerStartStabilization)
|
deadline := time.NewTimer(containerStartStabilization)
|
||||||
@@ -400,25 +406,25 @@ func waitContainerStable(ctx context.Context, cli containerSwitchClient, contain
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return checkContainerStableState(info, startedAt)
|
return checkContainerStableState(info, startedAt, restartCount)
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
info, err := cli.ContainerInspect(ctx, containerID)
|
info, err := cli.ContainerInspect(ctx, containerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := checkContainerStableState(info, startedAt); err != nil {
|
if err := checkContainerStableState(info, startedAt, restartCount); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkContainerStableState(info container.InspectResponse, startedAt string) error {
|
func checkContainerStableState(info container.InspectResponse, startedAt string, restartCount int) error {
|
||||||
if err := checkContainerRunningState(info); err != nil {
|
if err := checkContainerRunningState(info); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if info.State.Restarting || info.RestartCount != 0 {
|
if info.State.Restarting || info.RestartCount != restartCount {
|
||||||
return fmt.Errorf("container restarted %d times during startup", info.RestartCount)
|
return fmt.Errorf("container restart count changed from %d to %d during startup", restartCount, info.RestartCount)
|
||||||
}
|
}
|
||||||
if startedAt != "" && info.State.StartedAt != startedAt {
|
if startedAt != "" && info.State.StartedAt != startedAt {
|
||||||
return fmt.Errorf("container start time changed during startup")
|
return fmt.Errorf("container start time changed during startup")
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import (
|
|||||||
type CronjobService struct{}
|
type CronjobService struct{}
|
||||||
|
|
||||||
type ICronjobService interface {
|
type ICronjobService interface {
|
||||||
SearchWithPage(search dto.PageCronjob) (int64, interface{}, error)
|
SearchWithPage(search dto.PageCronjob, readOnly ...bool) (int64, interface{}, error)
|
||||||
SearchRecords(search dto.SearchRecord) (int64, interface{}, error)
|
SearchRecords(search dto.SearchRecord) (int64, interface{}, error)
|
||||||
Create(cronjobDto dto.CronjobOperate, operator string) error
|
Create(cronjobDto dto.CronjobOperate, operator string) error
|
||||||
LoadNextHandle(spec string) ([]string, error)
|
LoadNextHandle(spec string) ([]string, error)
|
||||||
@@ -50,7 +50,7 @@ func NewICronjobService() ICronjobService {
|
|||||||
return &CronjobService{}
|
return &CronjobService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *CronjobService) SearchWithPage(search dto.PageCronjob) (int64, interface{}, error) {
|
func (u *CronjobService) SearchWithPage(search dto.PageCronjob, readOnly ...bool) (int64, interface{}, error) {
|
||||||
total, cronjobs, err := cronjobRepo.Page(search.Page,
|
total, cronjobs, err := cronjobRepo.Page(search.Page,
|
||||||
search.PageSize,
|
search.PageSize,
|
||||||
repo.WithByGroups(search.GroupIDs),
|
repo.WithByGroups(search.GroupIDs),
|
||||||
@@ -83,6 +83,9 @@ func (u *CronjobService) SearchWithPage(search dto.PageCronjob) (int64, interfac
|
|||||||
if cronjob.Type == "snapshot" && len(cronjob.SnapshotRule) != 0 {
|
if cronjob.Type == "snapshot" && len(cronjob.SnapshotRule) != 0 {
|
||||||
_ = json.Unmarshal([]byte(cronjob.SnapshotRule), &item.SnapshotRule)
|
_ = json.Unmarshal([]byte(cronjob.SnapshotRule), &item.SnapshotRule)
|
||||||
}
|
}
|
||||||
|
if isDemoReadOnly(readOnly...) {
|
||||||
|
item.Secret = ""
|
||||||
|
}
|
||||||
dtoCronjobs = append(dtoCronjobs, item)
|
dtoCronjobs = append(dtoCronjobs, item)
|
||||||
}
|
}
|
||||||
return total, dtoCronjobs, err
|
return total, dtoCronjobs, err
|
||||||
@@ -538,11 +541,14 @@ func (u *CronjobService) CleanRecord(req dto.CronjobClean) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, del := range delRecords {
|
for _, del := range delRecords {
|
||||||
|
if del.Status == constant.StatusWaiting || del.Status == constant.StatusRunning {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := cronjobRepo.DeleteRecord(repo.WithByID(del.ID)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
_ = os.RemoveAll(del.Records)
|
_ = os.RemoveAll(del.Records)
|
||||||
}
|
}
|
||||||
if err := cronjobRepo.DeleteRecord(cronjobRepo.WithByJobID(int(req.CronjobID))); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
|
|||||||
record.Name = dbInfo.Database
|
record.Name = dbInfo.Database
|
||||||
record.DetailName = dbInfo.Name
|
record.DetailName = dbInfo.Name
|
||||||
record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
|
record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
|
||||||
|
record.Args = encodeBackupArgs(dbInfo.Args)
|
||||||
|
|
||||||
backupDir := path.Join(global.Dir.LocalBackupDir, fmt.Sprintf("tmp/database/%s/%s/%s", dbInfo.DBType, record.Name, dbInfo.Name))
|
backupDir := path.Join(global.Dir.LocalBackupDir, fmt.Sprintf("tmp/database/%s/%s/%s", dbInfo.DBType, record.Name, dbInfo.Name))
|
||||||
switch dbInfo.DBType {
|
switch dbInfo.DBType {
|
||||||
|
|||||||
@@ -184,10 +184,30 @@ func (u *DashboardService) LoadBaseInfo(ioOption string, netOption string) (*dto
|
|||||||
|
|
||||||
func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *dto.DashboardCurrent {
|
func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *dto.DashboardCurrent {
|
||||||
var currentInfo dto.DashboardCurrent
|
var currentInfo dto.DashboardCurrent
|
||||||
hostInfo, _ := psutil.HOST.GetHostInfo(false)
|
shotTime := time.Now()
|
||||||
currentInfo.Uptime = hostInfo.Uptime
|
hostInfo, err := psutil.HOST.GetHostInfo(false)
|
||||||
currentInfo.TimeSinceUptime = time.Unix(int64(hostInfo.BootTime), 0).Format(constant.DateTimeLayout)
|
if err != nil {
|
||||||
currentInfo.RunningTime = loadRunningTime(hostInfo.Uptime)
|
global.LOG.Errorf("load host info failed: %v", err)
|
||||||
|
currentInfo.ShotTime = shotTime
|
||||||
|
return ¤tInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
uptime := hostInfo.Uptime
|
||||||
|
var bootTime uint64
|
||||||
|
if now := shotTime.Unix(); now > 0 {
|
||||||
|
nowUnix := uint64(now)
|
||||||
|
if hostInfo.BootTime > 0 && hostInfo.BootTime <= nowUnix {
|
||||||
|
bootTime = hostInfo.BootTime
|
||||||
|
uptime = nowUnix - bootTime
|
||||||
|
} else if uptime <= nowUnix {
|
||||||
|
bootTime = nowUnix - uptime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentInfo.Uptime = uptime
|
||||||
|
currentInfo.RunningTime = loadRunningTime(uptime)
|
||||||
|
if bootTime > 0 {
|
||||||
|
currentInfo.TimeSinceUptime = time.Unix(int64(bootTime), 0).Format(constant.DateTimeLayout)
|
||||||
|
}
|
||||||
currentInfo.Procs = hostInfo.Procs
|
currentInfo.Procs = hostInfo.Procs
|
||||||
currentInfo.CPUTotal, _ = psutil.CPUInfo.GetLogicalCores(false)
|
currentInfo.CPUTotal, _ = psutil.CPUInfo.GetLogicalCores(false)
|
||||||
|
|
||||||
@@ -263,7 +283,7 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentInfo.ShotTime = time.Now()
|
currentInfo.ShotTime = shotTime
|
||||||
return ¤tInfo
|
return ¤tInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ import (
|
|||||||
type DatabaseService struct{}
|
type DatabaseService struct{}
|
||||||
|
|
||||||
type IDatabaseService interface {
|
type IDatabaseService interface {
|
||||||
Get(name string) (dto.DatabaseInfo, error)
|
Get(name string, readOnly ...bool) (dto.DatabaseInfo, error)
|
||||||
SearchWithPage(search dto.DatabaseSearch) (int64, interface{}, error)
|
SearchWithPage(search dto.DatabaseSearch, readOnly ...bool) (int64, interface{}, error)
|
||||||
CheckDatabase(req dto.DatabaseCreate) bool
|
CheckDatabase(req dto.DatabaseCreate) bool
|
||||||
Create(req dto.DatabaseCreate) error
|
Create(req dto.DatabaseCreate) error
|
||||||
Update(req dto.DatabaseUpdate) error
|
Update(req dto.DatabaseUpdate) error
|
||||||
@@ -39,7 +39,7 @@ func NewIDatabaseService() IDatabaseService {
|
|||||||
return &DatabaseService{}
|
return &DatabaseService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *DatabaseService) SearchWithPage(search dto.DatabaseSearch) (int64, interface{}, error) {
|
func (u *DatabaseService) SearchWithPage(search dto.DatabaseSearch, readOnly ...bool) (int64, interface{}, error) {
|
||||||
total, dbs, err := databaseRepo.Page(search.Page, search.PageSize,
|
total, dbs, err := databaseRepo.Page(search.Page, search.PageSize,
|
||||||
databaseRepo.WithTypeList(search.Type),
|
databaseRepo.WithTypeList(search.Type),
|
||||||
repo.WithByLikeName(search.Info),
|
repo.WithByLikeName(search.Info),
|
||||||
@@ -52,12 +52,16 @@ func (u *DatabaseService) SearchWithPage(search dto.DatabaseSearch) (int64, inte
|
|||||||
if err := copier.Copy(&item, &db); err != nil {
|
if err := copier.Copy(&item, &db); err != nil {
|
||||||
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||||
}
|
}
|
||||||
|
if isDemoReadOnly(readOnly...) {
|
||||||
|
item.Password = ""
|
||||||
|
item.ClientKey = ""
|
||||||
|
}
|
||||||
datas = append(datas, item)
|
datas = append(datas, item)
|
||||||
}
|
}
|
||||||
return total, datas, err
|
return total, datas, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *DatabaseService) Get(name string) (dto.DatabaseInfo, error) {
|
func (u *DatabaseService) Get(name string, readOnly ...bool) (dto.DatabaseInfo, error) {
|
||||||
var data dto.DatabaseInfo
|
var data dto.DatabaseInfo
|
||||||
remote, err := databaseRepo.Get(repo.WithByName(name))
|
remote, err := databaseRepo.Get(repo.WithByName(name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -66,6 +70,10 @@ func (u *DatabaseService) Get(name string) (dto.DatabaseInfo, error) {
|
|||||||
if err := copier.Copy(&data, &remote); err != nil {
|
if err := copier.Copy(&data, &remote); err != nil {
|
||||||
return data, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
return data, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||||
}
|
}
|
||||||
|
if isDemoReadOnly(readOnly...) {
|
||||||
|
data.Password = ""
|
||||||
|
data.ClientKey = ""
|
||||||
|
}
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import (
|
|||||||
type MongodbService struct{}
|
type MongodbService struct{}
|
||||||
|
|
||||||
type IMongodbService interface {
|
type IMongodbService interface {
|
||||||
SearchWithPage(search dto.MongodbDBSearch) (int64, interface{}, error)
|
SearchWithPage(search dto.MongodbDBSearch, readOnly ...bool) (int64, interface{}, error)
|
||||||
Create(ctx context.Context, req dto.MongodbDBCreate) (*model.DatabaseMongodb, error)
|
Create(ctx context.Context, req dto.MongodbDBCreate) (*model.DatabaseMongodb, error)
|
||||||
LoadFromRemote(req dto.MongodbLoadDB) error
|
LoadFromRemote(req dto.MongodbLoadDB) error
|
||||||
UpdateDescription(req dto.UpdateDescription) error
|
UpdateDescription(req dto.UpdateDescription) error
|
||||||
@@ -40,7 +40,7 @@ func NewIMongodbService() IMongodbService {
|
|||||||
return &MongodbService{}
|
return &MongodbService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *MongodbService) SearchWithPage(search dto.MongodbDBSearch) (int64, interface{}, error) {
|
func (u *MongodbService) SearchWithPage(search dto.MongodbDBSearch, readOnly ...bool) (int64, interface{}, error) {
|
||||||
total, mongodbs, err := mongodbRepo.Page(
|
total, mongodbs, err := mongodbRepo.Page(
|
||||||
search.Page,
|
search.Page,
|
||||||
search.PageSize,
|
search.PageSize,
|
||||||
@@ -54,6 +54,9 @@ func (u *MongodbService) SearchWithPage(search dto.MongodbDBSearch) (int64, inte
|
|||||||
if err := copier.Copy(&item, &mongodb); err != nil {
|
if err := copier.Copy(&item, &mongodb); err != nil {
|
||||||
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||||
}
|
}
|
||||||
|
if isDemoReadOnly(readOnly...) {
|
||||||
|
item.Password = ""
|
||||||
|
}
|
||||||
dtoMongodbs = append(dtoMongodbs, item)
|
dtoMongodbs = append(dtoMongodbs, item)
|
||||||
}
|
}
|
||||||
return total, dtoMongodbs, err
|
return total, dtoMongodbs, err
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ type IMysqlService interface {
|
|||||||
DeleteCheck(req dto.MysqlDBDeleteCheck) ([]dto.DBResource, error)
|
DeleteCheck(req dto.MysqlDBDeleteCheck) ([]dto.DBResource, error)
|
||||||
Delete(ctx context.Context, req dto.MysqlDBDelete) error
|
Delete(ctx context.Context, req dto.MysqlDBDelete) error
|
||||||
|
|
||||||
ListUsers(req dto.MysqlUserSearch) ([]dto.MysqlUser, error)
|
ListUsers(req dto.MysqlUserSearch, readOnly ...bool) ([]dto.MysqlUser, error)
|
||||||
ListGrants(req dto.MysqlUserSearch) ([]dto.MysqlGrant, error)
|
ListGrants(req dto.MysqlUserSearch) ([]dto.MysqlGrant, error)
|
||||||
ListGrantSummary(req dto.MysqlGrantSummarySearch) (map[string][]dto.MysqlUser, error)
|
ListGrantSummary(req dto.MysqlGrantSummarySearch) (map[string][]dto.MysqlUser, error)
|
||||||
CreateUser(req dto.MysqlUserCreate) error
|
CreateUser(req dto.MysqlUserCreate) error
|
||||||
@@ -506,7 +506,7 @@ func (u *MysqlService) Create(ctx context.Context, req dto.MysqlDBCreate) (*mode
|
|||||||
return &createItem, nil
|
return &createItem, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *MysqlService) ListUsers(req dto.MysqlUserSearch) ([]dto.MysqlUser, error) {
|
func (u *MysqlService) ListUsers(req dto.MysqlUserSearch, readOnly ...bool) ([]dto.MysqlUser, error) {
|
||||||
dbType, err := resolveDatabaseUserType(req.Database)
|
dbType, err := resolveDatabaseUserType(req.Database)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -520,10 +520,14 @@ func (u *MysqlService) ListUsers(req dto.MysqlUserSearch) ([]dto.MysqlUser, erro
|
|||||||
if isMysqlSystemUser(user.Username) {
|
if isMysqlSystemUser(user.Username) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
password := user.Password
|
||||||
|
if isDemoReadOnly(readOnly...) {
|
||||||
|
password = ""
|
||||||
|
}
|
||||||
res = append(res, dto.MysqlUser{
|
res = append(res, dto.MysqlUser{
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Host: user.Host,
|
Host: user.Host,
|
||||||
Password: user.Password,
|
Password: password,
|
||||||
Description: user.Description,
|
Description: user.Description,
|
||||||
IsDelete: user.IsDelete,
|
IsDelete: user.IsDelete,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import (
|
|||||||
type PostgresqlService struct{}
|
type PostgresqlService struct{}
|
||||||
|
|
||||||
type IPostgresqlService interface {
|
type IPostgresqlService interface {
|
||||||
SearchWithPage(search dto.PostgresqlDBSearch) (int64, interface{}, error)
|
SearchWithPage(search dto.PostgresqlDBSearch, readOnly ...bool) (int64, interface{}, error)
|
||||||
ListDBOption() ([]dto.PostgresqlOption, error)
|
ListDBOption() ([]dto.PostgresqlOption, error)
|
||||||
BindUser(req dto.PostgresqlBindUser) error
|
BindUser(req dto.PostgresqlBindUser) error
|
||||||
Create(ctx context.Context, req dto.PostgresqlDBCreate) (*model.DatabasePostgresql, error)
|
Create(ctx context.Context, req dto.PostgresqlDBCreate) (*model.DatabasePostgresql, error)
|
||||||
@@ -42,7 +42,7 @@ func NewIPostgresqlService() IPostgresqlService {
|
|||||||
return &PostgresqlService{}
|
return &PostgresqlService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *PostgresqlService) SearchWithPage(search dto.PostgresqlDBSearch) (int64, interface{}, error) {
|
func (u *PostgresqlService) SearchWithPage(search dto.PostgresqlDBSearch, readOnly ...bool) (int64, interface{}, error) {
|
||||||
total, postgresqls, err := postgresqlRepo.Page(search.Page, search.PageSize,
|
total, postgresqls, err := postgresqlRepo.Page(search.Page, search.PageSize,
|
||||||
postgresqlRepo.WithByPostgresqlName(search.Database),
|
postgresqlRepo.WithByPostgresqlName(search.Database),
|
||||||
repo.WithByLikeName(search.Info),
|
repo.WithByLikeName(search.Info),
|
||||||
@@ -54,6 +54,9 @@ func (u *PostgresqlService) SearchWithPage(search dto.PostgresqlDBSearch) (int64
|
|||||||
if err := copier.Copy(&item, &pg); err != nil {
|
if err := copier.Copy(&item, &pg); err != nil {
|
||||||
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||||
}
|
}
|
||||||
|
if isDemoReadOnly(readOnly...) {
|
||||||
|
item.Password = ""
|
||||||
|
}
|
||||||
dtoPostgresqls = append(dtoPostgresqls, item)
|
dtoPostgresqls = append(dtoPostgresqls, item)
|
||||||
}
|
}
|
||||||
return total, dtoPostgresqls, err
|
return total, dtoPostgresqls, err
|
||||||
|
|||||||
@@ -28,10 +28,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
rollbackPath = "1panel/tmp"
|
rollbackPath = "1panel/tmp"
|
||||||
upgradePath = "1panel/tmp/upgrade"
|
communityRestorePath = "1panel/tmp/community-restore"
|
||||||
uploadPath = "1panel/uploads"
|
upgradePath = "1panel/tmp/upgrade"
|
||||||
downloadPath = "1panel/download"
|
uploadPath = "1panel/uploads"
|
||||||
|
downloadPath = "1panel/download"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (u *DeviceService) Scan() dto.CleanData {
|
func (u *DeviceService) Scan() dto.CleanData {
|
||||||
@@ -58,7 +59,7 @@ func (u *DeviceService) Scan() dto.CleanData {
|
|||||||
|
|
||||||
SystemClean.BackupClean = loadBackupTree(fileOp)
|
SystemClean.BackupClean = loadBackupTree(fileOp)
|
||||||
|
|
||||||
rollBackTree := loadRollBackTree(fileOp)
|
rollBackTree := loadRollBackTree()
|
||||||
rollbackSize := uint64(0)
|
rollbackSize := uint64(0)
|
||||||
for _, rollback := range rollBackTree {
|
for _, rollback := range rollBackTree {
|
||||||
rollbackSize += rollback.Size
|
rollbackSize += rollback.Size
|
||||||
@@ -113,12 +114,15 @@ func (u *DeviceService) Clean(req []dto.Clean) {
|
|||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app"))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app"))
|
||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database"))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database"))
|
||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website"))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website"))
|
||||||
|
dropFileOrDir(path.Join(global.Dir.BaseDir, communityRestorePath))
|
||||||
case "rollback_app":
|
case "rollback_app":
|
||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app", item.Name))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app", item.Name))
|
||||||
case "rollback_database":
|
case "rollback_database":
|
||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database", item.Name))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database", item.Name))
|
||||||
case "rollback_website":
|
case "rollback_website":
|
||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website", item.Name))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website", item.Name))
|
||||||
|
case "rollback_community_restore":
|
||||||
|
dropFileOrDir(path.Join(global.Dir.BaseDir, communityRestorePath, item.Name))
|
||||||
|
|
||||||
case "upload":
|
case "upload":
|
||||||
dropFileOrDir(path.Join(global.Dir.BaseDir, uploadPath, item.Name))
|
dropFileOrDir(path.Join(global.Dir.BaseDir, uploadPath, item.Name))
|
||||||
@@ -214,6 +218,7 @@ func doSystemClean(taskItem *task.Task) func(t *task.Task) error {
|
|||||||
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "app"), taskItem, &size, &fileCount)
|
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "app"), taskItem, &size, &fileCount)
|
||||||
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "website"), taskItem, &size, &fileCount)
|
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "website"), taskItem, &size, &fileCount)
|
||||||
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "database"), taskItem, &size, &fileCount)
|
dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "database"), taskItem, &size, &fileCount)
|
||||||
|
dropWithTask(path.Join(global.Dir.BaseDir, communityRestorePath), taskItem, &size, &fileCount)
|
||||||
|
|
||||||
upgrades := path.Join(global.Dir.BaseDir, upgradePath)
|
upgrades := path.Join(global.Dir.BaseDir, upgradePath)
|
||||||
oldUpgradeFiles, _ := os.ReadDir(upgrades)
|
oldUpgradeFiles, _ := os.ReadDir(upgrades)
|
||||||
@@ -606,20 +611,21 @@ func isExactPathMatch(path string, excludePaths []string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadRollBackTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
func loadRollBackTree() []dto.CleanTree {
|
||||||
var treeData []dto.CleanTree
|
var treeData []dto.CleanTree
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "app"), "rollback_app", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "app"), "rollback_app")
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "website"), "rollback_website", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "website"), "rollback_website")
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "database"), "rollback_database", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "database"), "rollback_database")
|
||||||
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, communityRestorePath), "rollback_community_restore")
|
||||||
|
|
||||||
return treeData
|
return treeData
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadUploadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
func loadUploadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
||||||
var treeData []dto.CleanTree
|
var treeData []dto.CleanTree
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "app"), "upload_app", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "app"), "upload_app")
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "website"), "upload_website", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "website"), "upload_website")
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "database"), "upload_database", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "database"), "upload_database")
|
||||||
|
|
||||||
path5 := path.Join(global.Dir.BaseDir, uploadPath)
|
path5 := path.Join(global.Dir.BaseDir, uploadPath)
|
||||||
uploadTreeData := loadTreeWithAllFile(true, path5, "upload", path5, fileOp)
|
uploadTreeData := loadTreeWithAllFile(true, path5, "upload", path5, fileOp)
|
||||||
@@ -630,9 +636,9 @@ func loadUploadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
|||||||
|
|
||||||
func loadDownloadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
func loadDownloadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
||||||
var treeData []dto.CleanTree
|
var treeData []dto.CleanTree
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "app"), "download_app", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "app"), "download_app")
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "website"), "download_website", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "website"), "download_website")
|
||||||
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "database"), "download_database", fileOp)
|
treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "database"), "download_database")
|
||||||
|
|
||||||
path5 := path.Join(global.Dir.BaseDir, downloadPath)
|
path5 := path.Join(global.Dir.BaseDir, downloadPath)
|
||||||
uploadTreeData := loadTreeWithAllFile(true, path5, "download", path5, fileOp)
|
uploadTreeData := loadTreeWithAllFile(true, path5, "download", path5, fileOp)
|
||||||
@@ -814,16 +820,59 @@ func loadContainerTree() []dto.CleanTree {
|
|||||||
return treeData
|
return treeData
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadTreeWithCheck(treeData []dto.CleanTree, pathItem, treeType string, fileOp fileUtils.FileOp) []dto.CleanTree {
|
func loadTreeWithCheck(treeData []dto.CleanTree, pathItem, treeType string) []dto.CleanTree {
|
||||||
size, _ := fileOp.GetDirSize(pathItem)
|
list, size := loadTreeWithFileSize(true, pathItem, treeType, pathItem)
|
||||||
if size == 0 {
|
if len(list) == 0 || size == 0 {
|
||||||
return treeData
|
return treeData
|
||||||
}
|
}
|
||||||
list := loadTreeWithAllFile(true, pathItem, treeType, pathItem, fileOp)
|
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: treeType, Size: size, IsCheck: size > 0, Children: list, Type: treeType, IsRecommend: true, CanDelete: false})
|
||||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: treeType, Size: uint64(size), IsCheck: size > 0, Children: list, Type: treeType, IsRecommend: true, CanDelete: false})
|
|
||||||
return treeData
|
return treeData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadTreeWithFileSize(isCheck bool, originalPath, treeType, pathItem string) ([]dto.CleanTree, uint64) {
|
||||||
|
var (
|
||||||
|
lists []dto.CleanTree
|
||||||
|
total uint64
|
||||||
|
)
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(pathItem)
|
||||||
|
if err != nil {
|
||||||
|
return lists, total
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
item := dto.CleanTree{
|
||||||
|
ID: uuid.NewString(),
|
||||||
|
Label: entry.Name(),
|
||||||
|
Type: treeType,
|
||||||
|
Name: strings.TrimPrefix(path.Join(pathItem, entry.Name()), originalPath+"/"),
|
||||||
|
IsCheck: isCheck,
|
||||||
|
IsRecommend: isCheck,
|
||||||
|
CanDelete: true,
|
||||||
|
}
|
||||||
|
entryPath := path.Join(pathItem, entry.Name())
|
||||||
|
if entry.IsDir() {
|
||||||
|
children, size := loadTreeWithFileSize(isCheck, originalPath, treeType, entryPath)
|
||||||
|
if len(children) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item.Children = children
|
||||||
|
item.Size = size
|
||||||
|
} else {
|
||||||
|
info, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
item.Size = uint64(info.Size())
|
||||||
|
}
|
||||||
|
if item.Size == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
total += item.Size
|
||||||
|
lists = append(lists, item)
|
||||||
|
}
|
||||||
|
return lists, total
|
||||||
|
}
|
||||||
|
|
||||||
func loadTreeWithDir(isCheck bool, treeType, pathItem string, fileOp fileUtils.FileOp) []dto.CleanTree {
|
func loadTreeWithDir(isCheck bool, treeType, pathItem string, fileOp fileUtils.FileOp) []dto.CleanTree {
|
||||||
var lists []dto.CleanTree
|
var lists []dto.CleanTree
|
||||||
files, err := os.ReadDir(pathItem)
|
files, err := os.ReadDir(pathItem)
|
||||||
|
|||||||
@@ -61,18 +61,21 @@ func parseDevice(dev LsblkDevice) []response.DiskBasicInfo {
|
|||||||
|
|
||||||
var used, avail, totalSize string
|
var used, avail, totalSize string
|
||||||
var usePercent int
|
var usePercent int
|
||||||
isMounted := mountPoint != ""
|
isMounted := mountPoint != "" && mountPoint != "-"
|
||||||
isSystem := false
|
isSystem := false
|
||||||
|
|
||||||
if dev.Fstype == "LVM2_member" && len(dev.Children) > 0 {
|
if dev.Fstype == "LVM2_member" && len(dev.Children) > 0 {
|
||||||
for _, child := range dev.Children {
|
for _, child := range dev.Children {
|
||||||
if child.Type == "lvm" && child.Mountpoint != "" {
|
if child.Type == "lvm" && child.Mountpoint != "" && child.Mountpoint != "-" {
|
||||||
devicePath := "/dev/mapper/" + child.Name
|
totalSize, used, avail, usePercent, _ := getDiskUsageInfo(child.Mountpoint)
|
||||||
totalSize, used, avail, usePercent, _ := getDiskUsageInfo(devicePath)
|
childSize := child.Size
|
||||||
|
if totalSize != "" {
|
||||||
|
childSize = totalSize
|
||||||
|
}
|
||||||
|
|
||||||
childInfo := response.DiskBasicInfo{
|
childInfo := response.DiskBasicInfo{
|
||||||
Device: dev.Name,
|
Device: dev.Name,
|
||||||
Size: totalSize,
|
Size: childSize,
|
||||||
Model: dev.Model,
|
Model: dev.Model,
|
||||||
DiskType: diskType,
|
DiskType: diskType,
|
||||||
Filesystem: child.Fstype,
|
Filesystem: child.Fstype,
|
||||||
@@ -91,8 +94,7 @@ func parseDevice(dev LsblkDevice) []response.DiskBasicInfo {
|
|||||||
return list
|
return list
|
||||||
} else if isMounted {
|
} else if isMounted {
|
||||||
isSystem = isSystemDisk(mountPoint)
|
isSystem = isSystemDisk(mountPoint)
|
||||||
devicePath := "/dev/" + dev.Name
|
totalSize, used, avail, usePercent, _ = getDiskUsageInfo(mountPoint)
|
||||||
totalSize, used, avail, usePercent, _ = getDiskUsageInfo(devicePath)
|
|
||||||
if totalSize != "" {
|
if totalSize != "" {
|
||||||
size = totalSize
|
size = totalSize
|
||||||
}
|
}
|
||||||
@@ -229,9 +231,14 @@ func parseLsblkOutput(output string) ([]response.DiskBasicInfo, error) {
|
|||||||
size := fields["SIZE"]
|
size := fields["SIZE"]
|
||||||
|
|
||||||
if diskType == "lvm" {
|
if diskType == "lvm" {
|
||||||
total, used, avail, usePercent, _ := getDiskUsageInfo("/dev/mapper/" + name)
|
var total, used, avail string
|
||||||
if total != "" && fsType != "" {
|
var usePercent int
|
||||||
size = total
|
isMounted := mountPoint != "" && mountPoint != "-"
|
||||||
|
if isMounted {
|
||||||
|
total, used, avail, usePercent, _ = getDiskUsageInfo(mountPoint)
|
||||||
|
if total != "" && fsType != "" {
|
||||||
|
size = total
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lvmInfo := response.DiskBasicInfo{
|
lvmInfo := response.DiskBasicInfo{
|
||||||
@@ -246,7 +253,7 @@ func parseLsblkOutput(output string) ([]response.DiskBasicInfo, error) {
|
|||||||
Avail: avail,
|
Avail: avail,
|
||||||
UsePercent: usePercent,
|
UsePercent: usePercent,
|
||||||
MountPoint: mountPoint,
|
MountPoint: mountPoint,
|
||||||
IsMounted: mountPoint != "" && mountPoint != "-",
|
IsMounted: isMounted,
|
||||||
Serial: fields["SERIAL"],
|
Serial: fields["SERIAL"],
|
||||||
}
|
}
|
||||||
lvmMap[name] = lvmInfo
|
lvmMap[name] = lvmInfo
|
||||||
@@ -269,8 +276,8 @@ func parseLsblkOutput(output string) ([]response.DiskBasicInfo, error) {
|
|||||||
used, avail, totalSize string
|
used, avail, totalSize string
|
||||||
usePercent int
|
usePercent int
|
||||||
)
|
)
|
||||||
if mountPoint != "" {
|
if mountPoint != "" && mountPoint != "-" {
|
||||||
totalSize, used, avail, usePercent, _ = getDiskUsageInfo("/dev/" + name)
|
totalSize, used, avail, usePercent, _ = getDiskUsageInfo(mountPoint)
|
||||||
if totalSize != "" {
|
if totalSize != "" {
|
||||||
size = totalSize
|
size = totalSize
|
||||||
}
|
}
|
||||||
@@ -387,26 +394,39 @@ func getParentDevice(device string) string {
|
|||||||
return device
|
return device
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDiskUsageInfo(device string) (size, used, avail string, usePercent int, err error) {
|
func getDiskUsageInfo(mountPoint string) (size, used, avail string, usePercent int, err error) {
|
||||||
output, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout("df", "-h", device)
|
// Query by mount point instead of a reconstructed device path. The mount table may record
|
||||||
|
// a different device alias such as /dev/root.
|
||||||
|
output, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout("df", "-h", "-P", mountPoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", 0, nil
|
return "", "", "", 0, nil
|
||||||
}
|
}
|
||||||
|
return parseDiskUsageOutput(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDiskUsageOutput(output string) (size, used, avail string, usePercent int, err error) {
|
||||||
lines := strings.Split(strings.TrimSpace(output), "\n")
|
lines := strings.Split(strings.TrimSpace(output), "\n")
|
||||||
if len(lines) > 1 {
|
for i := len(lines) - 1; i >= 0; i-- {
|
||||||
output = lines[len(lines)-1]
|
fields := strings.Fields(lines[i])
|
||||||
|
for index, field := range fields {
|
||||||
|
if index < 3 || !strings.HasSuffix(field, "%") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
percent, parseErr := strconv.Atoi(strings.TrimSuffix(field, "%"))
|
||||||
|
if parseErr != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fields[index-3], fields[index-2], fields[index-1], percent, nil
|
||||||
|
}
|
||||||
|
for index, field := range fields {
|
||||||
|
if index < 3 || index+1 >= len(fields) || field != "-" || !strings.HasPrefix(fields[index+1], "/") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fields[index-3], fields[index-2], fields[index-1], 0, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := strings.Fields(output)
|
return "", "", "", 0, nil
|
||||||
if len(fields) >= 5 {
|
|
||||||
size = fields[1]
|
|
||||||
used = fields[2]
|
|
||||||
avail = fields[3]
|
|
||||||
usePercentStr := strings.TrimSuffix(fields[4], "%")
|
|
||||||
usePercent, _ = strconv.Atoi(usePercentStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return size, used, avail, usePercent, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatDisk(req dto.DiskFormatRequest) error {
|
func formatDisk(req dto.DiskFormatRequest) error {
|
||||||
|
|||||||
@@ -40,12 +40,14 @@ var (
|
|||||||
settingRepo = repo.NewISettingRepo()
|
settingRepo = repo.NewISettingRepo()
|
||||||
backupRepo = repo.NewIBackupRepo()
|
backupRepo = repo.NewIBackupRepo()
|
||||||
|
|
||||||
websiteRepo = repo.NewIWebsiteRepo()
|
websiteRepo = repo.NewIWebsiteRepo()
|
||||||
websiteDomainRepo = repo.NewIWebsiteDomainRepo()
|
websiteDomainRepo = repo.NewIWebsiteDomainRepo()
|
||||||
websiteDnsRepo = repo.NewIWebsiteDnsAccountRepo()
|
websiteDnsRepo = repo.NewIWebsiteDnsAccountRepo()
|
||||||
websiteSSLRepo = repo.NewISSLRepo()
|
websiteSSLRepo = repo.NewISSLRepo()
|
||||||
websiteAcmeRepo = repo.NewIAcmeAccountRepo()
|
websiteAcmeRepo = repo.NewIAcmeAccountRepo()
|
||||||
websiteCARepo = repo.NewIWebsiteCARepo()
|
websiteCARepo = repo.NewIWebsiteCARepo()
|
||||||
|
websiteTemplateRepo = repo.NewIWebsiteTemplateRepo()
|
||||||
|
websiteTemplateOutputRepo = repo.NewIWebsiteTemplateOutputRepo()
|
||||||
|
|
||||||
snapshotRepo = repo.NewISnapshotRepo()
|
snapshotRepo = repo.NewISnapshotRepo()
|
||||||
|
|
||||||
|
|||||||
+130
-27
@@ -16,6 +16,7 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
@@ -51,6 +52,8 @@ type FileService struct {
|
|||||||
|
|
||||||
const fileHistorySnapshotMaxSize = 10 * 1024 * 1024
|
const fileHistorySnapshotMaxSize = 10 * 1024 * 1024
|
||||||
|
|
||||||
|
var fileTransferLocks = newFileTransferLocks()
|
||||||
|
|
||||||
type IFileService interface {
|
type IFileService interface {
|
||||||
GetFileList(op request.FileOption) (response.FileInfo, error)
|
GetFileList(op request.FileOption) (response.FileInfo, error)
|
||||||
SearchUploadWithPage(req request.SearchUploadWithPage) (int64, interface{}, error)
|
SearchUploadWithPage(req request.SearchUploadWithPage) (int64, interface{}, error)
|
||||||
@@ -71,6 +74,7 @@ type IFileService interface {
|
|||||||
ChangeName(req request.FileRename) error
|
ChangeName(req request.FileRename) error
|
||||||
Wget(w request.FileWget) (string, error)
|
Wget(w request.FileWget) (string, error)
|
||||||
MvFile(m request.FileMove) error
|
MvFile(m request.FileMove) error
|
||||||
|
StopMvFile(taskID string) error
|
||||||
ChangeOwner(req request.FileRoleUpdate) error
|
ChangeOwner(req request.FileRoleUpdate) error
|
||||||
ChangeMode(op request.FileCreate) error
|
ChangeMode(op request.FileCreate) error
|
||||||
BatchChangeModeAndOwner(op request.FileRoleReq) error
|
BatchChangeModeAndOwner(op request.FileRoleReq) error
|
||||||
@@ -83,7 +87,7 @@ type IFileService interface {
|
|||||||
ConvertLog(req dto.PageInfo) (int64, []response.FileConvertLog, error)
|
ConvertLog(req dto.PageInfo) (int64, []response.FileConvertLog, error)
|
||||||
BatchGetRemarks(req request.FileRemarkBatch) map[string]string
|
BatchGetRemarks(req request.FileRemarkBatch) map[string]string
|
||||||
SetRemark(req request.FileRemarkUpdate) error
|
SetRemark(req request.FileRemarkUpdate) error
|
||||||
AISearch(req request.FileAISearch) (*response.FileAISearchResult, error)
|
AISearch(req request.FileAISearch, readOnly ...bool) (*response.FileAISearchResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -466,7 +470,7 @@ func (f *FileService) Compress(c request.FileCompress) error {
|
|||||||
|
|
||||||
func preflightCompressTool(compressType files.CompressType) error {
|
func preflightCompressTool(compressType files.CompressType) error {
|
||||||
switch compressType {
|
switch compressType {
|
||||||
case files.TarGz, files.Rar, files.X7z:
|
case files.Tar, files.Gz, files.Bz2, files.TarBz2, files.Tgz, files.TarGz, files.Xz, files.TarXz, files.Rar, files.X7z:
|
||||||
_, err := files.NewShellArchiver(compressType)
|
_, err := files.NewShellArchiver(compressType)
|
||||||
return err
|
return err
|
||||||
default:
|
default:
|
||||||
@@ -476,7 +480,7 @@ func preflightCompressTool(compressType files.CompressType) error {
|
|||||||
|
|
||||||
func preflightDecompressTool(decompressType files.CompressType) error {
|
func preflightDecompressTool(decompressType files.CompressType) error {
|
||||||
switch decompressType {
|
switch decompressType {
|
||||||
case files.Rar, files.X7z:
|
case files.Rar:
|
||||||
_, err := files.NewExtractShellArchiver(decompressType)
|
_, err := files.NewExtractShellArchiver(decompressType)
|
||||||
return err
|
return err
|
||||||
default:
|
default:
|
||||||
@@ -533,7 +537,10 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
|
|||||||
_ = os.RemoveAll(c.Dst)
|
_ = os.RemoveAll(c.Dst)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
if err := fo.Decompress(t.TaskCtx, c.Path, tempDst, files.CompressType(c.Type), c.Secret); err != nil {
|
if err := fo.DecompressWithOptions(t.TaskCtx, c.Path, tempDst, files.CompressType(c.Type), c.Secret, files.DecompressOptions{
|
||||||
|
PreserveOwner: true,
|
||||||
|
AllowCLIReextract: true,
|
||||||
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := fo.CreateDir(c.Dst, constant.DirPerm); err != nil {
|
if err := fo.CreateDir(c.Dst, constant.DirPerm); err != nil {
|
||||||
@@ -551,19 +558,42 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyDecompressTree(ctx context.Context, srcDir, dstDir string) error {
|
func copyDecompressTree(ctx context.Context, srcDir, dstDir string) error {
|
||||||
|
state := decompressCopyState{hardlinks: make(map[decompressFileIdentity]string)}
|
||||||
entries, err := os.ReadDir(srcDir)
|
entries, err := os.ReadDir(srcDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if err := copyDecompressEntry(ctx, filepath.Join(srcDir, entry.Name()), filepath.Join(dstDir, entry.Name())); err != nil {
|
if err := copyDecompressEntryWithState(ctx, filepath.Join(srcDir, entry.Name()), filepath.Join(dstDir, entry.Name()), &state); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type decompressFileIdentity struct {
|
||||||
|
device uint64
|
||||||
|
inode uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type decompressCopyState struct {
|
||||||
|
hardlinks map[decompressFileIdentity]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func decompressHardlinkIdentity(info os.FileInfo) (decompressFileIdentity, bool) {
|
||||||
|
stat, ok := info.Sys().(*syscall.Stat_t)
|
||||||
|
if !ok || stat.Nlink < 2 {
|
||||||
|
return decompressFileIdentity{}, false
|
||||||
|
}
|
||||||
|
return decompressFileIdentity{device: uint64(stat.Dev), inode: uint64(stat.Ino)}, true
|
||||||
|
}
|
||||||
|
|
||||||
func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr error) {
|
func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr error) {
|
||||||
|
state := decompressCopyState{hardlinks: make(map[decompressFileIdentity]string)}
|
||||||
|
return copyDecompressEntryWithState(ctx, srcPath, dstPath, &state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyDecompressEntryWithState(ctx context.Context, srcPath, dstPath string, state *decompressCopyState) (retErr error) {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -605,13 +635,16 @@ func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr e
|
|||||||
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
|
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := os.Chmod(dstPath, info.Mode().Perm()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
entries, err := os.ReadDir(srcPath)
|
entries, err := os.ReadDir(srcPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if err := copyDecompressEntry(ctx, filepath.Join(srcPath, entry.Name()), filepath.Join(dstPath, entry.Name())); err != nil {
|
if err := copyDecompressEntryWithState(ctx, filepath.Join(srcPath, entry.Name()), filepath.Join(dstPath, entry.Name()), state); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -634,6 +667,15 @@ func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr e
|
|||||||
if err := os.MkdirAll(filepath.Dir(dstPath), constant.DirPerm); err != nil {
|
if err := os.MkdirAll(filepath.Dir(dstPath), constant.DirPerm); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
identity, isHardlink := decompressHardlinkIdentity(info)
|
||||||
|
if !keepExistingFile && isHardlink {
|
||||||
|
if existingPath, ok := state.hardlinks[identity]; ok {
|
||||||
|
if err := os.Link(existingPath, dstPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
srcFile, err := os.Open(srcPath)
|
srcFile, err := os.Open(srcPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -658,6 +700,12 @@ func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr e
|
|||||||
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
|
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := os.Chmod(dstPath, info.Mode().Perm()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if isHardlink {
|
||||||
|
state.hardlinks[identity] = dstPath
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
|
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
|
||||||
}
|
}
|
||||||
@@ -667,7 +715,7 @@ func applyDecompressOwnership(srcPath, dstPath string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
stat, ok := info.Sys().(*unix.Stat_t)
|
stat, ok := info.Sys().(*syscall.Stat_t)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -863,17 +911,62 @@ func (f *FileService) Wget(w request.FileWget) (string, error) {
|
|||||||
|
|
||||||
func (f *FileService) MvFile(m request.FileMove) error {
|
func (f *FileService) MvFile(m request.FileMove) error {
|
||||||
fo := files.NewFileOp()
|
fo := files.NewFileOp()
|
||||||
|
if err := validateFileMove(fo, m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if m.TaskID == "" {
|
||||||
|
m.TaskID = common.GetUuid()
|
||||||
|
}
|
||||||
|
if !fileTransferLocks.Acquire(m.TaskID, getFileTransferPaths(m)) {
|
||||||
|
return buserr.New("TaskIsExecuting")
|
||||||
|
}
|
||||||
|
taskItem, err := task.NewTask(m.NewPath, task.TaskExec, task.TaskScopeTask, m.TaskID, 1)
|
||||||
|
if err != nil {
|
||||||
|
fileTransferLocks.Release(m.TaskID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
defer fileTransferLocks.Release(m.TaskID)
|
||||||
|
taskItem.AddSubTaskWithOps(m.NewPath, func(t *task.Task) error {
|
||||||
|
t.LogStart(m.NewPath)
|
||||||
|
err := f.moveFileWithContext(t.TaskCtx, m)
|
||||||
|
if err != nil && t.TaskCtx.Err() != nil {
|
||||||
|
return t.TaskCtx.Err()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}, nil, 0, 0)
|
||||||
|
_ = taskItem.Execute()
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileService) StopMvFile(taskID string) error {
|
||||||
|
if cancel, ok := global.LoadTaskCancel(taskID); ok {
|
||||||
|
cancel()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return buserr.New("TaskNotFound")
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateFileMove(fo files.FileOp, m request.FileMove) error {
|
||||||
if !fo.Stat(m.NewPath) {
|
if !fo.Stat(m.NewPath) {
|
||||||
return buserr.New("ErrPathNotFound")
|
return buserr.New("ErrPathNotFound")
|
||||||
}
|
}
|
||||||
for _, oldPath := range m.OldPaths {
|
for _, oldPath := range append(append([]string{}, m.OldPaths...), m.CoverPaths...) {
|
||||||
if !fo.Stat(oldPath) {
|
if !fo.Stat(oldPath) {
|
||||||
return buserr.WithName("ErrFileNotFound", oldPath)
|
return buserr.WithName("ErrFileNotFound", oldPath)
|
||||||
}
|
}
|
||||||
if oldPath == m.NewPath || strings.Contains(m.NewPath, filepath.Clean(oldPath)+"/") {
|
oldPath = filepath.Clean(oldPath)
|
||||||
|
newPath := filepath.Clean(m.NewPath)
|
||||||
|
if oldPath == newPath || strings.HasPrefix(newPath, oldPath+string(filepath.Separator)) {
|
||||||
return buserr.New("ErrMovePathFailed")
|
return buserr.New("ErrMovePathFailed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FileService) moveFileWithContext(ctx context.Context, m request.FileMove) error {
|
||||||
|
fo := files.NewFileOp()
|
||||||
type moveSnapshot struct {
|
type moveSnapshot struct {
|
||||||
path string
|
path string
|
||||||
content []byte
|
content []byte
|
||||||
@@ -889,13 +982,25 @@ func (f *FileService) MvFile(m request.FileMove) error {
|
|||||||
}
|
}
|
||||||
if len(m.CoverPaths) > 0 {
|
if len(m.CoverPaths) > 0 {
|
||||||
for _, src := range m.CoverPaths {
|
for _, src := range m.CoverPaths {
|
||||||
if err := fo.CopyAndReName(src, m.NewPath, "", true); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := fo.CopyAndReNameWithContext(ctx, src, m.NewPath, "", true); err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
global.LOG.Errorf("cut copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error())
|
global.LOG.Errorf("cut copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := fo.DeleteDir(src); err != nil {
|
||||||
|
removeErr := fmt.Errorf("remove merged source [%s] failed: %w", src, err)
|
||||||
|
errs = append(errs, removeErr)
|
||||||
|
global.LOG.Errorf("%s", removeErr.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := fo.Cut(m.OldPaths, m.NewPath, m.Name, m.Cover); err != nil {
|
if err := fo.CutWithContext(ctx, m.OldPaths, m.NewPath, m.Name, m.Cover); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, snapshot := range snapshots {
|
for _, snapshot := range snapshots {
|
||||||
@@ -906,18 +1011,18 @@ func (f *FileService) MvFile(m request.FileMove) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return aggregateFileMoveErrors(errs)
|
||||||
}
|
}
|
||||||
if m.Type == "copy" {
|
if m.Type == "copy" {
|
||||||
for _, src := range m.OldPaths {
|
for _, src := range m.OldPaths {
|
||||||
if err := fo.CopyAndReName(src, m.NewPath, m.Name, m.Cover); err != nil {
|
if err := fo.CopyAndReNameWithContext(ctx, src, m.NewPath, m.Name, m.Cover); err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
global.LOG.Errorf("copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error())
|
global.LOG.Errorf("copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(m.CoverPaths) > 0 {
|
if len(m.CoverPaths) > 0 {
|
||||||
for _, src := range m.CoverPaths {
|
for _, src := range m.CoverPaths {
|
||||||
if err := fo.CopyAndReName(src, m.NewPath, "", true); err != nil {
|
if err := fo.CopyAndReNameWithContext(ctx, src, m.NewPath, "", true); err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
global.LOG.Errorf("copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error())
|
global.LOG.Errorf("copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error())
|
||||||
}
|
}
|
||||||
@@ -925,14 +1030,7 @@ func (f *FileService) MvFile(m request.FileMove) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var errString string
|
return aggregateFileMoveErrors(errs)
|
||||||
for _, err := range errs {
|
|
||||||
errString += err.Error() + "\n"
|
|
||||||
}
|
|
||||||
if errString != "" {
|
|
||||||
return errors.New(errString)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readEditableFileHistoryContent(filePath string) ([]byte, os.FileMode, bool) {
|
func readEditableFileHistoryContent(filePath string) ([]byte, os.FileMode, bool) {
|
||||||
@@ -1462,7 +1560,7 @@ func (f *FileService) ConvertLog(req dto.PageInfo) (total int64, data []response
|
|||||||
return total, data, nil
|
return total, data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FileService) AISearch(req request.FileAISearch) (*response.FileAISearchResult, error) {
|
func (f *FileService) AISearch(req request.FileAISearch, readOnly ...bool) (*response.FileAISearchResult, error) {
|
||||||
root := filepath.Clean(strings.TrimSpace(req.Path))
|
root := filepath.Clean(strings.TrimSpace(req.Path))
|
||||||
if root == "" {
|
if root == "" {
|
||||||
return nil, buserr.WithDetail("ErrInvalidParams", "path is required", nil)
|
return nil, buserr.WithDetail("ErrInvalidParams", "path is required", nil)
|
||||||
@@ -1515,10 +1613,15 @@ func (f *FileService) AISearch(req request.FileAISearch) (*response.FileAISearch
|
|||||||
return nil, buserr.WithDetail("ErrFileAISearchBadPattern", err.Error(), nil)
|
return nil, buserr.WithDetail("ErrFileAISearchBadPattern", err.Error(), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, timeout, err := terminalai.LoadFileAIRuntimeConfig()
|
var cfg terminalai.GeneratorConfig
|
||||||
aiEnabled := err == nil
|
var timeout time.Duration
|
||||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
aiEnabled := false
|
||||||
return nil, err
|
if !isDemoReadOnly(readOnly...) {
|
||||||
|
cfg, timeout, err = terminalai.LoadFileAIRuntimeConfig()
|
||||||
|
aiEnabled = err == nil
|
||||||
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items, truncated, err := files.CollectDirInventory(root, containSub, maxItems)
|
items, truncated, err := files.CollectDirInventory(root, containSub, maxItems)
|
||||||
|
|||||||
@@ -37,14 +37,14 @@ var fileShareCodeRegexp = regexp.MustCompile(`^[A-Za-z0-9]{10,16}$`)
|
|||||||
|
|
||||||
type IFileShareService interface {
|
type IFileShareService interface {
|
||||||
Create(req request.FileShareCreate) (*response.FileShareInfo, error)
|
Create(req request.FileShareCreate) (*response.FileShareInfo, error)
|
||||||
Page(req dto.PageInfo) (int64, []response.FileShareInfo, error)
|
Page(req dto.PageInfo, readOnly ...bool) (int64, []response.FileShareInfo, error)
|
||||||
GetByPath(path string) (*response.FileShareInfo, error)
|
GetByPath(path string, readOnly ...bool) (*response.FileShareInfo, error)
|
||||||
GetByCode(code string) (*response.FileShareInfo, error)
|
GetByCode(code string, readOnly ...bool) (*response.FileShareInfo, error)
|
||||||
GetPublicByCode(code string) (*response.FileSharePublicInfo, error)
|
GetPublicByCode(code string, readOnly ...bool) (*response.FileSharePublicInfo, error)
|
||||||
DeleteByPath(path string) error
|
DeleteByPath(path string) error
|
||||||
SharePathCodeMap() (map[string]string, error)
|
SharePathCodeMap() (map[string]string, error)
|
||||||
Check(code, password string) error
|
Check(code, password string, readOnly ...bool) error
|
||||||
PrepareDownload(code, password string) (filePath, fileName string, err error)
|
PrepareDownload(code, password string, readOnly ...bool) (filePath, fileName string, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIFileShareService() IFileShareService {
|
func NewIFileShareService() IFileShareService {
|
||||||
@@ -212,14 +212,14 @@ func (s *FileShareService) Create(req request.FileShareCreate) (*response.FileSh
|
|||||||
return &res, nil
|
return &res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FileShareService) Page(req dto.PageInfo) (int64, []response.FileShareInfo, error) {
|
func (s *FileShareService) Page(req dto.PageInfo, readOnly ...bool) (int64, []response.FileShareInfo, error) {
|
||||||
items, err := fileShareRepo.All()
|
items, err := fileShareRepo.All()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
result := make([]response.FileShareInfo, 0, len(items))
|
result := make([]response.FileShareInfo, 0, len(items))
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
if err := s.pruneInvalidShare(item); err != nil {
|
if err := s.pruneInvalidShare(item, readOnly...); err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix {
|
if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix {
|
||||||
@@ -239,7 +239,7 @@ func (s *FileShareService) Page(req dto.PageInfo) (int64, []response.FileShareIn
|
|||||||
return int64(total), result[start:end], nil
|
return int64(total), result[start:end], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FileShareService) GetByPath(path string) (*response.FileShareInfo, error) {
|
func (s *FileShareService) GetByPath(path string, readOnly ...bool) (*response.FileShareInfo, error) {
|
||||||
item, err := fileShareRepo.GetFirst(fileShareRepo.WithByPath(strings.TrimSpace(path)))
|
item, err := fileShareRepo.GetFirst(fileShareRepo.WithByPath(strings.TrimSpace(path)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -247,7 +247,7 @@ func (s *FileShareService) GetByPath(path string) (*response.FileShareInfo, erro
|
|||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := s.pruneInvalidShare(item); err != nil {
|
if err := s.pruneInvalidShare(item, readOnly...); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix {
|
if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix {
|
||||||
@@ -258,7 +258,7 @@ func (s *FileShareService) GetByPath(path string) (*response.FileShareInfo, erro
|
|||||||
return &info, nil
|
return &info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FileShareService) GetByCode(code string) (*response.FileShareInfo, error) {
|
func (s *FileShareService) GetByCode(code string, readOnly ...bool) (*response.FileShareInfo, error) {
|
||||||
item, err := fileShareRepo.GetFirst(fileShareRepo.WithByCode(strings.TrimSpace(code)))
|
item, err := fileShareRepo.GetFirst(fileShareRepo.WithByCode(strings.TrimSpace(code)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -266,7 +266,7 @@ func (s *FileShareService) GetByCode(code string) (*response.FileShareInfo, erro
|
|||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := s.pruneInvalidShare(item); err != nil {
|
if err := s.pruneInvalidShare(item, readOnly...); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix {
|
if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix {
|
||||||
@@ -276,7 +276,7 @@ func (s *FileShareService) GetByCode(code string) (*response.FileShareInfo, erro
|
|||||||
return &info, nil
|
return &info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FileShareService) GetPublicByCode(code string) (*response.FileSharePublicInfo, error) {
|
func (s *FileShareService) GetPublicByCode(code string, readOnly ...bool) (*response.FileSharePublicInfo, error) {
|
||||||
item, err := fileShareRepo.GetFirst(fileShareRepo.WithByCode(strings.TrimSpace(code)))
|
item, err := fileShareRepo.GetFirst(fileShareRepo.WithByCode(strings.TrimSpace(code)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -284,7 +284,7 @@ func (s *FileShareService) GetPublicByCode(code string) (*response.FileSharePubl
|
|||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := s.pruneInvalidShare(item); err != nil {
|
if err := s.pruneInvalidShare(item, readOnly...); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix {
|
if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix {
|
||||||
@@ -324,32 +324,38 @@ func (s *FileShareService) SharePathCodeMap() (map[string]string, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FileShareService) Check(code, password string) error {
|
func (s *FileShareService) Check(code, password string, readOnly ...bool) error {
|
||||||
_, err := s.check(code, password)
|
_, err := s.check(code, password, readOnly...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FileShareService) PrepareDownload(code, password string) (string, string, error) {
|
func (s *FileShareService) PrepareDownload(code, password string, readOnly ...bool) (string, string, error) {
|
||||||
item, err := s.check(code, password)
|
item, err := s.check(code, password, readOnly...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
return item.Path, item.FileName, nil
|
return item.Path, item.FileName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FileShareService) pruneInvalidShare(item model.FileShare) error {
|
func (s *FileShareService) pruneInvalidShare(item model.FileShare, readOnly ...bool) error {
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
if item.ExpiresUnix > 0 && now > item.ExpiresUnix {
|
if item.ExpiresUnix > 0 && now > item.ExpiresUnix {
|
||||||
|
if isDemoReadOnly(readOnly...) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return fileShareRepo.Delete(repo.WithByID(item.ID))
|
return fileShareRepo.Delete(repo.WithByID(item.ID))
|
||||||
}
|
}
|
||||||
info, err := os.Stat(item.Path)
|
info, err := os.Stat(item.Path)
|
||||||
if err != nil || info.IsDir() {
|
if err != nil || info.IsDir() {
|
||||||
|
if isDemoReadOnly(readOnly...) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return fileShareRepo.Delete(repo.WithByID(item.ID))
|
return fileShareRepo.Delete(repo.WithByID(item.ID))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *FileShareService) check(code, password string) (*model.FileShare, error) {
|
func (s *FileShareService) check(code, password string, readOnly ...bool) (*model.FileShare, error) {
|
||||||
code = strings.TrimSpace(code)
|
code = strings.TrimSpace(code)
|
||||||
password = strings.TrimSpace(password)
|
password = strings.TrimSpace(password)
|
||||||
if code == "" {
|
if code == "" {
|
||||||
@@ -366,7 +372,9 @@ func (s *FileShareService) check(code, password string) (*model.FileShare, error
|
|||||||
|
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
if item.ExpiresUnix > 0 && now > item.ExpiresUnix {
|
if item.ExpiresUnix > 0 && now > item.ExpiresUnix {
|
||||||
_ = fileShareRepo.Delete(repo.WithByID(item.ID))
|
if !isDemoReadOnly(readOnly...) {
|
||||||
|
_ = fileShareRepo.Delete(repo.WithByID(item.ID))
|
||||||
|
}
|
||||||
return nil, buserr.New("ErrFileShareExpired")
|
return nil, buserr.New("ErrFileShareExpired")
|
||||||
}
|
}
|
||||||
if item.PasswordHash != "" {
|
if item.PasswordHash != "" {
|
||||||
@@ -377,7 +385,9 @@ func (s *FileShareService) check(code, password string) (*model.FileShare, error
|
|||||||
|
|
||||||
info, err := os.Stat(item.Path)
|
info, err := os.Stat(item.Path)
|
||||||
if err != nil || info.IsDir() {
|
if err != nil || info.IsDir() {
|
||||||
_ = fileShareRepo.Delete(repo.WithByID(item.ID))
|
if !isDemoReadOnly(readOnly...) {
|
||||||
|
_ = fileShareRepo.Delete(repo.WithByID(item.ID))
|
||||||
|
}
|
||||||
return nil, buserr.New("ErrFileSharePath")
|
return nil, buserr.New("ErrFileSharePath")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fileTransferLockSet struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
paths map[string][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFileTransferLocks() *fileTransferLockSet {
|
||||||
|
return &fileTransferLockSet{paths: make(map[string][]string)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fileTransferLockSet) Acquire(taskID string, transferPaths []string) bool {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
for _, activePaths := range s.paths {
|
||||||
|
for _, activePath := range activePaths {
|
||||||
|
for _, transferPath := range transferPaths {
|
||||||
|
if fileTransferPathsOverlap(activePath, transferPath) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.paths[taskID] = transferPaths
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *fileTransferLockSet) Release(taskID string) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
delete(s.paths, taskID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFileTransferPaths(req request.FileMove) []string {
|
||||||
|
paths := make([]string, 0, 1+len(req.OldPaths)+len(req.CoverPaths))
|
||||||
|
paths = append(paths, req.NewPath)
|
||||||
|
paths = append(paths, req.OldPaths...)
|
||||||
|
paths = append(paths, req.CoverPaths...)
|
||||||
|
|
||||||
|
unique := make(map[string]struct{}, len(paths))
|
||||||
|
result := make([]string, 0, len(paths))
|
||||||
|
for _, item := range paths {
|
||||||
|
item = filepath.Clean(item)
|
||||||
|
if _, ok := unique[item]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
unique[item] = struct{}{}
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileTransferPathsOverlap(first, second string) bool {
|
||||||
|
return first == second || strings.HasPrefix(first, second+string(filepath.Separator)) || strings.HasPrefix(second, first+string(filepath.Separator))
|
||||||
|
}
|
||||||
|
|
||||||
|
func aggregateFileMoveErrors(errs []error) error {
|
||||||
|
if len(errs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var errString strings.Builder
|
||||||
|
for _, err := range errs {
|
||||||
|
errString.WriteString(err.Error())
|
||||||
|
errString.WriteByte('\n')
|
||||||
|
}
|
||||||
|
return errors.New(errString.String())
|
||||||
|
}
|
||||||
@@ -3,14 +3,12 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
@@ -28,7 +26,6 @@ type IFirewallService interface {
|
|||||||
SearchWithPage(search dto.RuleSearch) (int64, interface{}, error)
|
SearchWithPage(search dto.RuleSearch) (int64, interface{}, error)
|
||||||
OperateFirewall(req dto.FirewallOperation) error
|
OperateFirewall(req dto.FirewallOperation) error
|
||||||
OperatePortRule(req dto.PortRuleOperate, reload bool) error
|
OperatePortRule(req dto.PortRuleOperate, reload bool) error
|
||||||
OperateForwardRule(req dto.ForwardRuleOperate) error
|
|
||||||
OperateAddressRule(req dto.AddrRuleOperate, reload bool) error
|
OperateAddressRule(req dto.AddrRuleOperate, reload bool) error
|
||||||
UpdatePortRule(req dto.PortRuleUpdate) error
|
UpdatePortRule(req dto.PortRuleUpdate) error
|
||||||
UpdateAddrRule(req dto.AddrRuleUpdate) error
|
UpdateAddrRule(req dto.AddrRuleUpdate) error
|
||||||
@@ -84,8 +81,6 @@ func (u *FirewallService) SearchWithPage(req dto.RuleSearch) (int64, interface{}
|
|||||||
switch req.Type {
|
switch req.Type {
|
||||||
case "port":
|
case "port":
|
||||||
rules, err = client.ListPort()
|
rules, err = client.ListPort()
|
||||||
case "forward":
|
|
||||||
rules, err = client.ListForward()
|
|
||||||
case "address":
|
case "address":
|
||||||
rules, err = client.ListAddress()
|
rules, err = client.ListAddress()
|
||||||
}
|
}
|
||||||
@@ -317,96 +312,6 @@ func (u *FirewallService) OperatePortRule(req dto.PortRuleOperate, reload bool)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *FirewallService) OperateForwardRule(req dto.ForwardRuleOperate) error {
|
|
||||||
client, err := firewall.NewFirewallClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
rules, _ := client.ListForward()
|
|
||||||
i := 0
|
|
||||||
for _, rule := range rules {
|
|
||||||
shouldKeep := true
|
|
||||||
for i := range req.Rules {
|
|
||||||
reqRule := &req.Rules[i]
|
|
||||||
if reqRule.TargetIP == "" {
|
|
||||||
reqRule.TargetIP = "127.0.0.1"
|
|
||||||
}
|
|
||||||
|
|
||||||
if reqRule.Operation == "remove" {
|
|
||||||
for _, proto := range strings.Split(reqRule.Protocol, "/") {
|
|
||||||
if reqRule.Port == rule.Port &&
|
|
||||||
reqRule.TargetPort == rule.TargetPort &&
|
|
||||||
reqRule.TargetIP == rule.TargetIP &&
|
|
||||||
proto == rule.Protocol &&
|
|
||||||
reqRule.Interface == rule.Interface {
|
|
||||||
shouldKeep = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if shouldKeep {
|
|
||||||
rules[i] = rule
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rules = rules[:i]
|
|
||||||
|
|
||||||
for _, rule := range rules {
|
|
||||||
for _, reqRule := range req.Rules {
|
|
||||||
if reqRule.Operation == "remove" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, proto := range strings.Split(reqRule.Protocol, "/") {
|
|
||||||
if reqRule.Port == rule.Port &&
|
|
||||||
reqRule.TargetPort == rule.TargetPort &&
|
|
||||||
reqRule.TargetIP == rule.TargetIP &&
|
|
||||||
proto == rule.Protocol &&
|
|
||||||
reqRule.Interface == rule.Interface {
|
|
||||||
return buserr.New("ErrRecordExist")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.SliceStable(req.Rules, func(i, j int) bool {
|
|
||||||
if req.Rules[i].Operation == "remove" && req.Rules[j].Operation != "remove" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if req.Rules[i].Operation != "remove" && req.Rules[j].Operation == "remove" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
n1, _ := strconv.Atoi(req.Rules[i].Num)
|
|
||||||
n2, _ := strconv.Atoi(req.Rules[j].Num)
|
|
||||||
return n1 > n2
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, r := range req.Rules {
|
|
||||||
for _, p := range strings.Split(r.Protocol, "/") {
|
|
||||||
if r.TargetIP == "" {
|
|
||||||
r.TargetIP = "127.0.0.1"
|
|
||||||
}
|
|
||||||
if err = client.PortForward(fireClient.Forward{
|
|
||||||
Num: r.Num,
|
|
||||||
Protocol: p,
|
|
||||||
Port: r.Port,
|
|
||||||
TargetIP: r.TargetIP,
|
|
||||||
TargetPort: r.TargetPort,
|
|
||||||
Interface: r.Interface,
|
|
||||||
}, r.Operation); err != nil {
|
|
||||||
if req.ForceDelete {
|
|
||||||
global.LOG.Error(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *FirewallService) OperateAddressRule(req dto.AddrRuleOperate, reload bool) error {
|
func (u *FirewallService) OperateAddressRule(req dto.AddrRuleOperate, reload bool) error {
|
||||||
client, err := firewall.NewFirewallClient()
|
client, err := firewall.NewFirewallClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -521,7 +426,7 @@ func OperateFirewallPort(oldPorts, newPorts []int) error {
|
|||||||
return client.Reload()
|
return client.Reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *FirewallService) operatePort(client firewall.FirewallClient, req dto.PortRuleOperate) error {
|
func (u *FirewallService) operatePort(client firewall.FilterClient, req dto.PortRuleOperate) error {
|
||||||
var fireInfo fireClient.FireInfo
|
var fireInfo fireClient.FireInfo
|
||||||
if err := copier.Copy(&fireInfo, &req); err != nil {
|
if err := copier.Copy(&fireInfo, &req); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -589,7 +494,7 @@ func (u *FirewallService) loadPortByApp() []portOfApp {
|
|||||||
return datas
|
return datas
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *FirewallService) cleanUnUsedData(client firewall.FirewallClient) {
|
func (u *FirewallService) cleanUnUsedData(client firewall.FilterClient) {
|
||||||
list, _ := client.ListPort()
|
list, _ := client.ListPort()
|
||||||
addressList, _ := client.ListAddress()
|
addressList, _ := client.ListAddress()
|
||||||
list = append(list, addressList...)
|
list = append(list, addressList...)
|
||||||
@@ -613,7 +518,7 @@ func (u *FirewallService) cleanUnUsedData(client firewall.FirewallClient) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *FirewallService) addPortsBeforeStart(client firewall.FirewallClient) error {
|
func (u *FirewallService) addPortsBeforeStart(client firewall.FilterClient) error {
|
||||||
if client.Name() == "iptables" {
|
if client.Name() == "iptables" {
|
||||||
isInit, _ := iptables.LoadInitStatus("iptables", "base")
|
isInit, _ := iptables.LoadInitStatus("iptables", "base")
|
||||||
if !isInit {
|
if !isInit {
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ func syncFirewallPortWhiteListAfterUpdate(oldValue string) error {
|
|||||||
return syncFirewallClientPortWhiteList(client, oldPortWhiteList, portWhiteList)
|
return syncFirewallClientPortWhiteList(client, oldPortWhiteList, portWhiteList)
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncFirewallClientPortWhiteList(client firewall.FirewallClient, oldPortWhiteList, portWhiteList []firewallPortWhitelist) error {
|
func syncFirewallClientPortWhiteList(client firewall.FilterClient, oldPortWhiteList, portWhiteList []firewallPortWhitelist) error {
|
||||||
oldPorts := firewallPortWhiteListMap(oldPortWhiteList)
|
oldPorts := firewallPortWhiteListMap(oldPortWhiteList)
|
||||||
newPorts := firewallPortWhiteListMap(portWhiteList)
|
newPorts := firewallPortWhiteListMap(portWhiteList)
|
||||||
for _, item := range oldPortWhiteList {
|
for _, item := range oldPortWhiteList {
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||||
|
forwardClient "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IForwardingService interface {
|
||||||
|
LoadBaseInfo() (dto.FirewallBaseInfo, error)
|
||||||
|
SearchWithPage(search dto.ForwardRuleSearch) (int64, interface{}, error)
|
||||||
|
Operate(req dto.ForwardRuleOperate) error
|
||||||
|
Enable() error
|
||||||
|
Replay() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForwardingService struct {
|
||||||
|
adapterFactory func() (forwardClient.Adapter, error)
|
||||||
|
filterFactory func() (firewall.FilterClient, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIForwardingService() IForwardingService {
|
||||||
|
return &ForwardingService{
|
||||||
|
adapterFactory: newForwardingAdapter,
|
||||||
|
filterFactory: firewall.NewFirewallClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newForwardingAdapter() (forwardClient.Adapter, error) {
|
||||||
|
client, err := firewall.NewFirewallClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return forwardClient.NewAdapter(client.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) LoadBaseInfo() (dto.FirewallBaseInfo, error) {
|
||||||
|
baseInfo := dto.FirewallBaseInfo{Version: "-", Name: "-"}
|
||||||
|
adapter, err := s.adapterFactory()
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("load forwarding failed, err: %v", err)
|
||||||
|
return baseInfo, nil
|
||||||
|
}
|
||||||
|
filter, err := s.filterFactory()
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("load firewall status failed, err: %v", err)
|
||||||
|
return baseInfo, nil
|
||||||
|
}
|
||||||
|
baseInfo.IsExist = true
|
||||||
|
baseInfo.Name = adapter.Name()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(2)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
baseInfo.PingStatus = firewall.LoadPingStatus()
|
||||||
|
baseInfo.Version, _ = filter.Version()
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
baseInfo.IsActive, _ = filter.Status()
|
||||||
|
baseInfo.IsInit, baseInfo.IsBind = adapter.InitStatus()
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
return baseInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) SearchWithPage(req dto.ForwardRuleSearch) (int64, interface{}, error) {
|
||||||
|
adapter, err := s.adapterFactory()
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
rules, err := adapter.List()
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
if req.Strategy != "" {
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var filtered []forwardClient.Rule
|
||||||
|
for _, rule := range rules {
|
||||||
|
if req.Info != "" && !strings.Contains(rule.Port, req.Info) &&
|
||||||
|
!strings.Contains(rule.TargetPort, req.Info) && !strings.Contains(rule.TargetIP, req.Info) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, rule)
|
||||||
|
}
|
||||||
|
total := len(filtered)
|
||||||
|
start, end := (req.Page-1)*req.PageSize, req.Page*req.PageSize
|
||||||
|
if start > total {
|
||||||
|
return int64(total), make([]dto.ForwardRule, 0), nil
|
||||||
|
}
|
||||||
|
if end > total {
|
||||||
|
end = total
|
||||||
|
}
|
||||||
|
pageRules := filtered[start:end]
|
||||||
|
var items []dto.ForwardRule
|
||||||
|
if pageRules != nil {
|
||||||
|
items = make([]dto.ForwardRule, 0, len(pageRules))
|
||||||
|
}
|
||||||
|
for _, rule := range pageRules {
|
||||||
|
items = append(items, dto.ForwardRule{
|
||||||
|
Num: rule.Num,
|
||||||
|
Protocol: rule.Protocol,
|
||||||
|
Port: rule.Port,
|
||||||
|
TargetIP: rule.TargetIP,
|
||||||
|
TargetPort: rule.TargetPort,
|
||||||
|
Interface: rule.Interface,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return int64(total), items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) Operate(req dto.ForwardRuleOperate) error {
|
||||||
|
adapter, err := s.adapterFactory()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rules, _ := adapter.List()
|
||||||
|
kept := rules[:0]
|
||||||
|
for _, rule := range rules {
|
||||||
|
shouldKeep := true
|
||||||
|
for i := range req.Rules {
|
||||||
|
reqRule := &req.Rules[i]
|
||||||
|
if reqRule.TargetIP == "" {
|
||||||
|
reqRule.TargetIP = "127.0.0.1"
|
||||||
|
}
|
||||||
|
if reqRule.Operation == "remove" && requestMatchesForwardRule(*reqRule, rule) {
|
||||||
|
shouldKeep = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if shouldKeep {
|
||||||
|
kept = append(kept, rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rule := range kept {
|
||||||
|
for _, reqRule := range req.Rules {
|
||||||
|
if reqRule.Operation != "remove" && requestMatchesForwardRule(reqRule, rule) {
|
||||||
|
return buserr.New("ErrRecordExist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.SliceStable(req.Rules, func(i, j int) bool {
|
||||||
|
if req.Rules[i].Operation == "remove" && req.Rules[j].Operation != "remove" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if req.Rules[i].Operation != "remove" && req.Rules[j].Operation == "remove" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
n1, _ := strconv.Atoi(req.Rules[i].Num)
|
||||||
|
n2, _ := strconv.Atoi(req.Rules[j].Num)
|
||||||
|
return n1 > n2
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, rule := range req.Rules {
|
||||||
|
for _, protocol := range strings.Split(rule.Protocol, "/") {
|
||||||
|
targetIP := rule.TargetIP
|
||||||
|
if targetIP == "" {
|
||||||
|
targetIP = "127.0.0.1"
|
||||||
|
}
|
||||||
|
err := adapter.Operate(forwardClient.Rule{
|
||||||
|
Num: rule.Num,
|
||||||
|
Protocol: protocol,
|
||||||
|
Port: rule.Port,
|
||||||
|
TargetIP: targetIP,
|
||||||
|
TargetPort: rule.TargetPort,
|
||||||
|
Interface: rule.Interface,
|
||||||
|
}, rule.Operation)
|
||||||
|
if err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if req.ForceDelete {
|
||||||
|
global.LOG.Error(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestMatchesForwardRule(req dto.ForwardRuleOperation, rule forwardClient.Rule) bool {
|
||||||
|
for _, protocol := range strings.Split(req.Protocol, "/") {
|
||||||
|
if req.Port == rule.Port && req.TargetPort == rule.TargetPort && req.TargetIP == rule.TargetIP &&
|
||||||
|
protocol == rule.Protocol && req.Interface == rule.Interface {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) Enable() error {
|
||||||
|
adapter, err := s.adapterFactory()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := adapter.Enable(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if adapter.Name() != "firewalld" {
|
||||||
|
_ = settingRepo.Update("IptablesForwardStatus", constant.StatusEnable)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingService) Replay() error {
|
||||||
|
adapter, err := s.adapterFactory()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := adapter.Replay(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if adapter.Name() == "firewalld" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
status, _ := settingRepo.GetValueByKey("IptablesForwardStatus")
|
||||||
|
if status == constant.StatusEnable {
|
||||||
|
return adapter.Enable()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||||
|
forwardClient "github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
)
|
||||||
|
|
||||||
|
type forwardingCall struct {
|
||||||
|
rule forwardClient.Rule
|
||||||
|
operation string
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeForwardingAdapter struct {
|
||||||
|
name string
|
||||||
|
rules []forwardClient.Rule
|
||||||
|
listErr error
|
||||||
|
operateErr error
|
||||||
|
calls []forwardingCall
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeForwardingAdapter) Name() string { return f.name }
|
||||||
|
|
||||||
|
func (f *fakeForwardingAdapter) List() ([]forwardClient.Rule, error) {
|
||||||
|
return append([]forwardClient.Rule(nil), f.rules...), f.listErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeForwardingAdapter) Operate(rule forwardClient.Rule, operation string) error {
|
||||||
|
f.calls = append(f.calls, forwardingCall{rule: rule, operation: operation})
|
||||||
|
return f.operateErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeForwardingAdapter) Enable() error { return nil }
|
||||||
|
func (f *fakeForwardingAdapter) InitStatus() (bool, bool) { return true, true }
|
||||||
|
func (f *fakeForwardingAdapter) Replay() error { return nil }
|
||||||
|
|
||||||
|
func forwardingServiceWithAdapter(adapter forwardClient.Adapter) *ForwardingService {
|
||||||
|
return &ForwardingService{
|
||||||
|
adapterFactory: func() (forwardClient.Adapter, error) { return adapter, nil },
|
||||||
|
filterFactory: firewall.NewFirewallClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForwardingAndFilterInterfacesAreSeparated(t *testing.T) {
|
||||||
|
filterType := reflect.TypeOf((*firewall.FilterClient)(nil)).Elem()
|
||||||
|
for _, method := range []string{"ListForward", "PortForward", "EnableForward"} {
|
||||||
|
if _, ok := filterType.MethodByName(method); ok {
|
||||||
|
t.Fatalf("filter interface still exposes %s", method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
firewallServiceType := reflect.TypeOf((*IFirewallService)(nil)).Elem()
|
||||||
|
if _, ok := firewallServiceType.MethodByName("OperateForwardRule"); ok {
|
||||||
|
t.Fatal("firewall service still owns forwarding writes")
|
||||||
|
}
|
||||||
|
forwardingServiceType := reflect.TypeOf((*IForwardingService)(nil)).Elem()
|
||||||
|
for _, method := range []string{"LoadBaseInfo", "SearchWithPage", "Operate", "Enable", "Replay"} {
|
||||||
|
if _, ok := forwardingServiceType.MethodByName(method); !ok {
|
||||||
|
t.Fatalf("forwarding service missing %s", method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForwardingInitRequestContract(t *testing.T) {
|
||||||
|
req := dto.IptablesOp{Name: "1PANEL_FORWARD", Operate: "init-forward"}
|
||||||
|
if err := validator.New().Struct(req); err != nil {
|
||||||
|
t.Fatalf("frontend forwarding initialization request must remain valid: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForwardingSearchPreservesAPIShapeAndPagination(t *testing.T) {
|
||||||
|
adapter := &fakeForwardingAdapter{name: "iptables", rules: []forwardClient.Rule{
|
||||||
|
{Num: "1", Protocol: "tcp", Port: "8080", TargetIP: "10.0.0.2", TargetPort: "80", Interface: "eth0"},
|
||||||
|
{Num: "2", Protocol: "udp", Port: "5353", TargetIP: "127.0.0.1", TargetPort: "53"},
|
||||||
|
}}
|
||||||
|
service := forwardingServiceWithAdapter(adapter)
|
||||||
|
total, value, err := service.SearchWithPage(dto.ForwardRuleSearch{PageInfo: dto.PageInfo{Page: 1, PageSize: 10}, Info: "10.0.0.2"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if total != 1 {
|
||||||
|
t.Fatalf("got total %d want 1", total)
|
||||||
|
}
|
||||||
|
items, ok := value.([]dto.ForwardRule)
|
||||||
|
if !ok || len(items) != 1 || items[0].Port != "8080" {
|
||||||
|
t.Fatalf("unexpected items: %#v", value)
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(items[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var fields map[string]interface{}
|
||||||
|
if err := json.Unmarshal(data, &fields); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
wantFields := []string{"id", "chain", "family", "address", "port", "protocol", "strategy", "num", "targetIP", "targetPort", "interface", "usedStatus", "description"}
|
||||||
|
for _, field := range wantFields {
|
||||||
|
if _, ok := fields[field]; !ok {
|
||||||
|
t.Fatalf("forward response dropped compatibility field %q: %s", field, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForwardingOperatePreservesDuplicateAndOrderingContracts(t *testing.T) {
|
||||||
|
existing := &fakeForwardingAdapter{name: "ufw", rules: []forwardClient.Rule{
|
||||||
|
{Protocol: "tcp", Port: "8080", TargetIP: "127.0.0.1", TargetPort: "80"},
|
||||||
|
}}
|
||||||
|
service := forwardingServiceWithAdapter(existing)
|
||||||
|
err := service.Operate(dto.ForwardRuleOperate{Rules: []dto.ForwardRuleOperation{{
|
||||||
|
Operation: "add", Protocol: "tcp", Port: "8080", TargetPort: "80",
|
||||||
|
}}})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("duplicate forwarding rule must be rejected")
|
||||||
|
}
|
||||||
|
if len(existing.calls) != 0 {
|
||||||
|
t.Fatalf("duplicate check wrote forwarding state: %#v", existing.calls)
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter := &fakeForwardingAdapter{name: "iptables"}
|
||||||
|
service = forwardingServiceWithAdapter(adapter)
|
||||||
|
err = service.Operate(dto.ForwardRuleOperate{Rules: []dto.ForwardRuleOperation{
|
||||||
|
{Operation: "add", Protocol: "tcp/udp", Port: "9000", TargetIP: "10.0.0.2", TargetPort: "90"},
|
||||||
|
{Operation: "remove", Num: "1", Protocol: "tcp", Port: "8001", TargetIP: "10.0.0.2", TargetPort: "81"},
|
||||||
|
{Operation: "remove", Num: "3", Protocol: "tcp", Port: "8003", TargetIP: "10.0.0.2", TargetPort: "83"},
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := []forwardingCall{
|
||||||
|
{operation: "remove", rule: forwardClient.Rule{Num: "3", Protocol: "tcp", Port: "8003", TargetIP: "10.0.0.2", TargetPort: "83"}},
|
||||||
|
{operation: "remove", rule: forwardClient.Rule{Num: "1", Protocol: "tcp", Port: "8001", TargetIP: "10.0.0.2", TargetPort: "81"}},
|
||||||
|
{operation: "add", rule: forwardClient.Rule{Protocol: "tcp", Port: "9000", TargetIP: "10.0.0.2", TargetPort: "90"}},
|
||||||
|
{operation: "add", rule: forwardClient.Rule{Protocol: "udp", Port: "9000", TargetIP: "10.0.0.2", TargetPort: "90"}},
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(adapter.calls, want) {
|
||||||
|
t.Fatalf("operation order changed\ngot %#v\nwant %#v", adapter.calls, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForwardingSearchReturnsAdapterError(t *testing.T) {
|
||||||
|
wantErr := errors.New("list failed")
|
||||||
|
service := forwardingServiceWithAdapter(&fakeForwardingAdapter{name: "firewalld", listErr: wantErr})
|
||||||
|
_, _, err := service.SearchWithPage(dto.ForwardRuleSearch{PageInfo: dto.PageInfo{Page: 1, PageSize: 20}})
|
||||||
|
if !errors.Is(err, wantErr) {
|
||||||
|
t.Fatalf("got %v want %v", err, wantErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
-24
@@ -1,6 +1,8 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
@@ -19,9 +21,10 @@ type FtpService struct{}
|
|||||||
|
|
||||||
type IFtpService interface {
|
type IFtpService interface {
|
||||||
LoadBaseInfo() (dto.FtpBaseInfo, error)
|
LoadBaseInfo() (dto.FtpBaseInfo, error)
|
||||||
SearchWithPage(search dto.SearchWithPage) (int64, interface{}, error)
|
SearchWithPage(search dto.SearchWithPage, readOnly ...bool) (int64, interface{}, error)
|
||||||
Operate(operation string) error
|
Operate(operation string) error
|
||||||
Create(req dto.FtpCreate) (uint, error)
|
Create(req dto.FtpCreate) (uint, error)
|
||||||
|
CreateWebsite(req dto.FtpCreate) (uint, error)
|
||||||
Delete(req dto.BatchDeleteReq) error
|
Delete(req dto.BatchDeleteReq) error
|
||||||
Update(req dto.FtpUpdate) error
|
Update(req dto.FtpUpdate) error
|
||||||
Sync() error
|
Sync() error
|
||||||
@@ -34,11 +37,7 @@ func NewIFtpService() IFtpService {
|
|||||||
|
|
||||||
func (f *FtpService) LoadBaseInfo() (dto.FtpBaseInfo, error) {
|
func (f *FtpService) LoadBaseInfo() (dto.FtpBaseInfo, error) {
|
||||||
var baseInfo dto.FtpBaseInfo
|
var baseInfo dto.FtpBaseInfo
|
||||||
client, err := toolbox.NewFtpClient()
|
baseInfo.IsActive, baseInfo.IsExist = toolbox.FtpStatus()
|
||||||
if err != nil {
|
|
||||||
return baseInfo, err
|
|
||||||
}
|
|
||||||
baseInfo.IsActive, baseInfo.IsExist = client.Status()
|
|
||||||
return baseInfo, nil
|
return baseInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +74,7 @@ func (u *FtpService) Operate(operation string) error {
|
|||||||
return client.Operate(operation)
|
return client.Operate(operation)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FtpService) SearchWithPage(req dto.SearchWithPage) (int64, interface{}, error) {
|
func (f *FtpService) SearchWithPage(req dto.SearchWithPage, readOnly ...bool) (int64, interface{}, error) {
|
||||||
total, lists, err := ftpRepo.Page(req.Page, req.PageSize, ftpRepo.WithLikeUser(req.Info), repo.WithOrderDesc("created_at"))
|
total, lists, err := ftpRepo.Page(req.Page, req.PageSize, ftpRepo.WithLikeUser(req.Info), repo.WithOrderDesc("created_at"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
@@ -86,7 +85,11 @@ func (f *FtpService) SearchWithPage(req dto.SearchWithPage) (int64, interface{},
|
|||||||
if err := copier.Copy(&item, &user); err != nil {
|
if err := copier.Copy(&item, &user); err != nil {
|
||||||
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||||
}
|
}
|
||||||
item.Password, _ = encrypt.StringDecrypt(item.Password)
|
if isDemoReadOnly(readOnly...) {
|
||||||
|
item.Password = ""
|
||||||
|
} else {
|
||||||
|
item.Password, _ = encrypt.StringDecrypt(item.Password)
|
||||||
|
}
|
||||||
users = append(users, item)
|
users = append(users, item)
|
||||||
}
|
}
|
||||||
return total, users, err
|
return total, users, err
|
||||||
@@ -99,7 +102,7 @@ func (f *FtpService) Sync() error {
|
|||||||
}
|
}
|
||||||
lists, err := client.LoadList()
|
lists, err := client.LoadList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
listsInDB, err := ftpRepo.GetList()
|
listsInDB, err := ftpRepo.GetList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -113,13 +116,24 @@ func (f *FtpService) Sync() error {
|
|||||||
for _, item := range lists {
|
for _, item := range lists {
|
||||||
if itemInDB, ok := currentData[item.User]; ok {
|
if itemInDB, ok := currentData[item.User]; ok {
|
||||||
sameData[item.User] = struct{}{}
|
sameData[item.User] = struct{}{}
|
||||||
if item.Path != itemInDB.Path || item.Status != itemInDB.Status {
|
if item.Path != itemInDB.Path || item.Status != itemInDB.Status || item.UID != itemInDB.UID || item.GID != itemInDB.GID {
|
||||||
if err := ftpRepo.Update(itemInDB.ID, map[string]interface{}{"path": item.Path, "status": item.Status}); err != nil {
|
if err := ftpRepo.Update(itemInDB.ID, map[string]interface{}{
|
||||||
|
"path": item.Path,
|
||||||
|
"status": item.Status,
|
||||||
|
"uid": item.UID,
|
||||||
|
"gid": item.GID,
|
||||||
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := ftpRepo.Create(&model.Ftp{User: item.User, Path: item.Path, Status: item.Status}); err != nil {
|
if err := ftpRepo.Create(&model.Ftp{
|
||||||
|
User: item.User,
|
||||||
|
Path: item.Path,
|
||||||
|
Status: item.Status,
|
||||||
|
UID: item.UID,
|
||||||
|
GID: item.GID,
|
||||||
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,6 +147,21 @@ func (f *FtpService) Sync() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *FtpService) Create(req dto.FtpCreate) (uint, error) {
|
func (f *FtpService) Create(req dto.FtpCreate) (uint, error) {
|
||||||
|
return f.create(req, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FtpService) CreateWebsite(req dto.FtpCreate) (uint, error) {
|
||||||
|
return f.create(req, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FtpService) create(req dto.FtpCreate, website bool) (uint, error) {
|
||||||
|
if err := toolbox.ValidateFtpRootPath(req.Path); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
client, err := toolbox.NewFtpClient()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
if _, err := os.Stat(req.Path); err != nil {
|
if _, err := os.Stat(req.Path); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(req.Path, os.ModePerm); err != nil {
|
if err := os.MkdirAll(req.Path, os.ModePerm); err != nil {
|
||||||
@@ -150,20 +179,28 @@ func (f *FtpService) Create(req dto.FtpCreate) (uint, error) {
|
|||||||
if userInDB.ID != 0 {
|
if userInDB.ID != 0 {
|
||||||
return 0, buserr.New("ErrRecordExist")
|
return 0, buserr.New("ErrRecordExist")
|
||||||
}
|
}
|
||||||
client, err := toolbox.NewFtpClient()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if err := client.UserAdd(req.User, req.Password, req.Path); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
var ftp model.Ftp
|
var ftp model.Ftp
|
||||||
if err := copier.Copy(&ftp, &req); err != nil {
|
if err := copier.Copy(&ftp, &req); err != nil {
|
||||||
return 0, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
return 0, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||||
}
|
}
|
||||||
|
uid, gid := uint(constant.WebsiteUID), uint(constant.WebsiteGID)
|
||||||
|
if !website {
|
||||||
|
uid, gid, err = toolbox.EnsureStandaloneFtpIdentity()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := client.UserAdd(req.User, req.Password, req.Path, uid, gid); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
ftp.Status = constant.StatusEnable
|
ftp.Status = constant.StatusEnable
|
||||||
ftp.Password = pass
|
ftp.Password = pass
|
||||||
|
ftp.UID = uid
|
||||||
|
ftp.GID = gid
|
||||||
if err := ftpRepo.Create(&ftp); err != nil {
|
if err := ftpRepo.Create(&ftp); err != nil {
|
||||||
|
if rollbackErr := client.UserDel(req.User); rollbackErr != nil {
|
||||||
|
return 0, errors.Join(err, fmt.Errorf("rollback FTP user %s failed: %w", req.User, rollbackErr))
|
||||||
|
}
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return ftp.ID, nil
|
return ftp.ID, nil
|
||||||
@@ -186,6 +223,13 @@ func (f *FtpService) Delete(req dto.BatchDeleteReq) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *FtpService) Update(req dto.FtpUpdate) error {
|
func (f *FtpService) Update(req dto.FtpUpdate) error {
|
||||||
|
if err := toolbox.ValidateFtpRootPath(req.Path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
client, err := toolbox.NewFtpClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if _, err := os.Stat(req.Path); err != nil {
|
if _, err := os.Stat(req.Path); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(req.Path, os.ModePerm); err != nil {
|
if err := os.MkdirAll(req.Path, os.ModePerm); err != nil {
|
||||||
@@ -209,10 +253,6 @@ func (f *FtpService) Update(req dto.FtpUpdate) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := toolbox.NewFtpClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
needReload := false
|
needReload := false
|
||||||
updates := make(map[string]interface{})
|
updates := make(map[string]interface{})
|
||||||
if req.Password != passItem {
|
if req.Password != passItem {
|
||||||
@@ -230,7 +270,11 @@ func (f *FtpService) Update(req dto.FtpUpdate) error {
|
|||||||
needReload = true
|
needReload = true
|
||||||
}
|
}
|
||||||
if req.Path != ftpItem.Path {
|
if req.Path != ftpItem.Path {
|
||||||
if err := client.SetPath(ftpItem.User, req.Path); err != nil {
|
uid, gid := ftpItem.UID, ftpItem.GID
|
||||||
|
if uid == 0 || gid == 0 {
|
||||||
|
uid, gid = uint(constant.WebsiteUID), uint(constant.WebsiteGID)
|
||||||
|
}
|
||||||
|
if err := client.SetPath(ftpItem.User, req.Path, uid, gid); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
updates["path"] = req.Path
|
updates["path"] = req.Path
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ type IHostService interface {
|
|||||||
TestByInfo(req dto.HostConnTest) bool
|
TestByInfo(req dto.HostConnTest) bool
|
||||||
GetHostByID(id uint) (*dto.HostInfo, error)
|
GetHostByID(id uint) (*dto.HostInfo, error)
|
||||||
SearchForTree(search dto.SearchForTree) ([]dto.HostTree, error)
|
SearchForTree(search dto.SearchForTree) ([]dto.HostTree, error)
|
||||||
SearchWithPage(search dto.SearchPageWithGroup) (int64, interface{}, error)
|
SearchWithPage(search dto.SearchPageWithGroup, readOnly ...bool) (int64, interface{}, error)
|
||||||
Create(req dto.HostOperate) (*dto.HostInfo, error)
|
Create(req dto.HostOperate) (*dto.HostInfo, error)
|
||||||
Update(id uint, upMap map[string]interface{}) (*dto.HostInfo, error)
|
Update(id uint, upMap map[string]interface{}) (*dto.HostInfo, error)
|
||||||
Delete(id []uint) error
|
Delete(id []uint) error
|
||||||
@@ -111,7 +111,7 @@ func (u *HostService) TestLocalConn(id uint) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *HostService) SearchWithPage(req dto.SearchPageWithGroup) (int64, interface{}, error) {
|
func (u *HostService) SearchWithPage(req dto.SearchPageWithGroup, readOnly ...bool) (int64, interface{}, error) {
|
||||||
var options []repo.DBOption
|
var options []repo.DBOption
|
||||||
if len(req.Info) != 0 {
|
if len(req.Info) != 0 {
|
||||||
options = append(options, hostRepo.WithByInfo(req.Info))
|
options = append(options, hostRepo.WithByInfo(req.Info))
|
||||||
@@ -155,6 +155,11 @@ func (u *HostService) SearchWithPage(req dto.SearchPageWithGroup) (int64, interf
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if isDemoReadOnly(readOnly...) {
|
||||||
|
item.Password = ""
|
||||||
|
item.PrivateKey = ""
|
||||||
|
item.PassPhrase = ""
|
||||||
|
}
|
||||||
dtoHosts = append(dtoHosts, item)
|
dtoHosts = append(dtoHosts, item)
|
||||||
}
|
}
|
||||||
return total, dtoHosts, err
|
return total, dtoHosts, err
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import (
|
|||||||
type ImageRepoService struct{}
|
type ImageRepoService struct{}
|
||||||
|
|
||||||
type IImageRepoService interface {
|
type IImageRepoService interface {
|
||||||
Page(search dto.SearchWithPage) (int64, interface{}, error)
|
Page(search dto.SearchWithPage, readOnly ...bool) (int64, interface{}, error)
|
||||||
List() ([]dto.ImageRepoOption, error)
|
List() ([]dto.ImageRepoOption, error)
|
||||||
Login(req dto.OperateByID) error
|
Login(req dto.OperateByID) error
|
||||||
Create(req dto.ImageRepoCreate) error
|
Create(req dto.ImageRepoCreate) error
|
||||||
@@ -36,10 +36,13 @@ func NewIImageRepoService() IImageRepoService {
|
|||||||
return &ImageRepoService{}
|
return &ImageRepoService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ImageRepoService) Page(req dto.SearchWithPage) (int64, interface{}, error) {
|
func (u *ImageRepoService) Page(req dto.SearchWithPage, readOnly ...bool) (int64, interface{}, error) {
|
||||||
total, ops, err := imageRepoRepo.Page(req.Page, req.PageSize, repo.WithByLikeName(req.Info), repo.WithOrderDesc("created_at"))
|
total, ops, err := imageRepoRepo.Page(req.Page, req.PageSize, repo.WithByLikeName(req.Info), repo.WithOrderDesc("created_at"))
|
||||||
var dtoOps []dto.ImageRepoInfo
|
var dtoOps []dto.ImageRepoInfo
|
||||||
for _, op := range ops {
|
for _, op := range ops {
|
||||||
|
if isDemoReadOnly(readOnly...) {
|
||||||
|
op.Password = ""
|
||||||
|
}
|
||||||
var item dto.ImageRepoInfo
|
var item dto.ImageRepoInfo
|
||||||
if err := copier.Copy(&item, &op); err != nil {
|
if err := copier.Copy(&item, &op); err != nil {
|
||||||
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user