Compare commits

...
Author SHA1 Message Date
wanghe-fit2cloud c3c7fe320d feat: support enterprise demo mode 2026-08-19 17:31:41 +08:00
14728f889e fix(ssh): correlate disconnect logs by client endpoint (#13581)
* fix(ssh): correlate disconnect logs by client endpoint

* fix(ssh): scope endpoint correlation to active sessions

---------

Co-authored-by: JayLee-sre <1.18655426e+08+JayLee-sre@users.noreply.github.com>
Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-08-19 10:20:16 +08:00
CityFun ff199245b0 feat: add some translate (#13585) 2026-08-18 18:46:27 +08:00
蘭 667807e249 fix: Fix large file/slow network upload timeout in file management (#13584) 2026-08-18 18:12:42 +08:00
Jet.andJayLee-sre 63e09c8c47 docs: name Halo in website deployment overview (#13580)
Co-authored-by: JayLee-sre <1.18655426e+08+JayLee-sre@users.noreply.github.com>
2026-08-18 09:49:11 +08:00
ssongliu 17a8835d59 feat: support Ascend 910B GPU monitoring (#13579)
* feat: support Ascend 910B GPU monitoring

* chore: remove GPU test files
2026-08-17 17:38:43 +08:00
王贺 b306bfa77a fix: correct disk usage for device aliases (#13570) 2026-08-17 15:20:15 +08:00
蘭 b1eff2a893 feat: change some translate (#13568) 2026-08-17 13:58:53 +08:00
王贺 5ac7c80881 fix: support underscores and hyphens in website default documents (#13551) 2026-08-14 10:55:44 +08:00
CityFun d402f67fc3 feat: Optimize application upgrade logic (#13549)
* feat: Optimize application upgrade logic

* feat: Optimize application upgrade logic

* feat: Optimize application upgrade logic
2026-08-13 18:28:59 +08:00
Chen, Ting-An c13793c445 fix(i18n): polish Traditional Chinese agent copy (#13536) 2026-08-12 15:32:39 +08:00
CityFun daa3f6b206 chore: Update dependencies (#13528) 2026-08-12 15:29:40 +08:00
maninhill a2d85c911d Revise user statistics and AI agents limit in README (#13525)
Updated user statistics and modified AI agents limit in the feature table.
2026-08-11 09:39:10 +08:00
蘭 1b76c91e1b fix: Fix file loss issues when copying or moving large directories (#13524) 2026-08-10 22:07:45 +08:00
maninhill d663a4397a Update README for clarity and security features (#13516)
Removed redundant mention of AI gateway in the Full-Stack AI Management section and added WAF to the security features.
2026-08-10 10:04:41 +08:00
maninhill d1558c5eae Revise README for clarity and feature updates (#13515)
Updated the README to enhance the description of 1Panel's features, including AI management, security, and backup capabilities. Adjusted the Pro Edition feature comparison to include the Enterprise edition.
2026-08-09 09:14:52 +08:00
maninhill 0da4f77a2e Revise README.zh-Hans.md for feature updates (#13512)
Updated the README in Chinese to reflect new features and improvements in 1Panel, including AI management capabilities and enhanced security features.
2026-08-07 22:51:57 +08:00
ssongliu e7ef35740c fix: align compose project name handling (#13500) 2026-08-07 12:44:29 +08:00
ssongliu b0d561e33b feat: show license expiration alert on dashboard (#13499) 2026-08-07 10:22:23 +08:00
CityFun 75b362fa9b chore: update dependencies (#13497) 2026-08-06 21:47:20 +08:00
蘭 466f373ef6 feat: change some translate (#13496) 2026-08-06 18:27:27 +08:00
ssongliu 4489641b54 perf(snapshot): hard link local recovery archive (#13494) 2026-08-06 16:08:58 +08:00
蘭 1971d9dec2 fix: handle external login state and emit readiness event (#13493)
* fix: handle external login state and emit readiness event

* fix: handle external login state and improve SAML2 logout response handling
2026-08-06 13:53:36 +08:00
CityFun c38d741770 fix: Fix an issue where the website monitoring directory was not completely removed when deleting a website. (#13492) 2026-08-06 13:52:53 +08:00
蘭 122a474032 feat: enhance alert log search with start and end time filters (#13489) 2026-08-05 21:13:25 +08:00
CityFun 54854e99e7 feat: change deepseek api url (#13487) 2026-08-05 17:10:34 +08:00
CityFun e01fb7c905 fix: Fixed bug with website template (#13484) 2026-08-05 16:56:08 +08:00
CityFun 83a3675a0c feat: QwenPaw support config username/password (#13478) 2026-08-05 16:35:09 +08:00
ssongliu 9dfa451fae fix: adjust host column display (#13477) 2026-08-05 13:52:33 +08:00
ssongliu 9204a287fd fix: improve community restore loading state (#13475)
* fix: improve community restore loading state

* fix: resume license page initialization after restore
2026-08-05 13:52:14 +08:00
f027507f9a fix: preserve active cronjob records during cleanup (#13474)
* fix: preserve active cronjob records during cleanup

* fix: delete cronjob records before removing files

Co-authored-by: ssongliu <73214554+ssongliu@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-08-05 11:32:20 +08:00
ssongliu 01aee89f3b fix: correct process start time in LXC (#13473)
* fix: correct process start time in LXC

* fix: reject unresolved process start times
2026-08-05 11:22:26 +08:00
CityFun 17285d4397 fix: Fixed issue with upgrade openresty failed (#13470)
* fix: Fixed issue with upgrade openresty failed

* fix: Fixed issue with upgrade openresty failed
2026-08-05 09:37:02 +08:00
蘭 91ae846418 feat: enhance archiving and extraction capabilities with additional compression formats and ownership preservation (#13458)
* feat: enhance archiving and extraction capabilities with additional compression formats and ownership preservation

* feat: enhance archiving and extraction capabilities with additional compression formats and ownership preservation

* feat: enhance archiving and extraction capabilities with additional compression formats and ownership preservation
2026-08-04 17:34:50 +08:00
ssongliu 1eb429631d fix: resolve compose project names consistently (#13468) 2026-08-04 17:17:07 +08:00
CityFun 6584e3b868 chore: update dependencies (#13467) 2026-08-04 16:46:27 +08:00
ssongliu 14e2294db9 feat: refine node health check settings (#13463) 2026-08-04 15:09:34 +08:00
ssongliu 26b69bc208 fix: bypass cached route in user info navigation (#13462)
* fix: bypass cached route in user info navigation

* fix: avoid caching entrance route
2026-08-04 15:09:17 +08:00
CityFun 2111d4c16b style: change table page config (#13456) 2026-08-04 10:45:13 +08:00
蘭 b682835b4e fix: Implement file sharing password processing and remote download file name processing (#13452) 2026-08-04 09:37:26 +08:00
蘭 c34ac2f31e feat: change some translate (#13451) 2026-08-04 09:37:08 +08:00
CityFun c28047374a feat: Fix the issue where website configuration files fail to restore from backups. (#13445) 2026-08-03 15:26:20 +08:00
ssongliu 60d16609f7 fix: clarify API trusted proxy placeholder (#13444) 2026-08-03 14:30:50 +08:00
CityFun 02ca9347dc feat: Update Xiaomi Models (#13443) 2026-08-03 14:28:26 +08:00
ssongliu d0187994ee fix: ignore empty directories in device clean scan (#13442)
* fix: ignore empty directories in device clean scan

* fix: skip zero-size device clean entries
2026-08-03 14:06:56 +08:00
蘭 be672d604f feat:Support logging in with oidc and saml2 (#13441)
* feat(auth): implement OIDC authentication endpoints and error handling

* feat(auth): add OIDC provider discovery endpoint and related functionality

* feat(auth): add SAML2 authentication support and related functionality

* feat(auth): add LDAP authentication support and related functionality

* fix(auth): improve login keydown handler for better event handling
2026-08-03 13:43:01 +08:00
CityFun 16e3d496eb Merge branch 'dev-v2' into pr@dev-v2@common (#13436) 2026-08-01 14:08:03 +08:00
ssongliu 9159ab842d feat: support filtering app store composes (#13432)
* feat: support filtering app store composes

* fix: initialize app store filter during setup
2026-07-31 18:27:10 +08:00
ssongliu 0d8835d494 fix: constrain dashboard column height (#13433) 2026-07-31 18:26:02 +08:00
ssongliu 9f9e3aacfc feat: add Community Edition restore UI (#13431) 2026-07-31 18:15:24 +08:00
CityFun 7bd11fe73e feat: Keep Website List Order Stable After Editing (#13424) 2026-07-31 11:05:23 +08:00
BugPleaseGoandCityFun e11dc5fadd Add Website Template (#13400)
* feat: Add Website Template

* fix: Don't display the template list

* feat: Add Mcp TopList

* docs: Remove Mcp TopList

* Add more languages

---------

Co-authored-by: CityFun <31820853+zhengkunwang223@users.noreply.github.com>
2026-07-31 10:01:20 +08:00
HynoR f35b0deb29 refactor(firewall): extract port forwarding subsystem (#13347)
Port forwarding no longer shares the filter client. FilterClient keeps only
filter capabilities, and forwarding gets its own adapter, service and boot
replay:

- utils/firewall/forwarding holds the provider adapters. firewalld uses native
  forward-port, ufw and iptables share the NAT implementation moved out of
  client/iptables/forward.go.
- service/forwarding.go owns base info, search, operate, enable and replay.
  The API keeps its routes and dispatches on name/type/operate.
- init/firewall replays forwarding through that service instead of loading NAT
  rule files inline.

Also adds 1PANEL_FORWARD to the IptablesOp name enum: the frontend already
sends {"name":"1PANEL_FORWARD","operate":"init-forward"} and the validator
rejected it with 400 before reaching the service. Besides that, the only
observable difference is that a forward-tab search no longer triggers the
port/address record cleanup goroutine on the side.
2026-07-30 14:07:41 +08:00
ssongliu 33b3eecb95 feat: unify pin actions (#13417) 2026-07-30 09:39:47 +08:00
CityFun 6ac7a5f167 feat: Optimize the application upgrade logic. (#13416) 2026-07-29 17:44:50 +08:00
CityFun a5fbbfc460 feat: Optimize the application upgrade logic. (#13415) 2026-07-29 17:19:52 +08:00
ssongliu 563df3da71 fix: support trusted proxies for API allowlist (#13409) 2026-07-29 16:48:59 +08:00
ssongliu 13e6bc4fac feat: separate website and standalone FTP identities (#13412) 2026-07-29 16:41:43 +08:00
ssongliu 357d77856a fix: align dashboard uptime with boot time (#13410)
* fix: align dashboard uptime with boot time

* fix: handle invalid dashboard boot time
2026-07-29 16:36:05 +08:00
ssongliu 4279339189 fix: simplify cronjob record duration display (#13398) 2026-07-29 10:57:40 +08:00
ssongliuandCopilot Autofix powered by AI 380033dfe0 fix: support MySQL backup GTID options (#13407)
* fix: support MySQL backup GTID options

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-29 10:42:21 +08:00
ssongliu 97d383ed12 feat: add explicit FTP identity initialization (#13390) 2026-07-28 17:07:57 +08:00
ssongliu 52e6a63ebc fix: improve host system log pagination (#13395) 2026-07-28 17:07:22 +08:00
CityFun 7506e709e2 Add plugin management support to OpenClaw. (#13388) 2026-07-28 14:20:55 +08:00
蘭 cf37de66fc feat(auth): add LDAP authentication support and related configurations (#13385)
* feat(auth): add LDAP authentication support and related configurations

* feat(ldap): implement LDAP synchronization features and enhance user import logic

* feat(auth): add authSource and authSourceStatus to user information

* feat(i18n): update LDAP synchronization error messages in multiple languages

* feat(i18n): update LDAP synchronization error messages in multiple languages

* feat(i18n): update LDAP synchronization error messages in multiple languages
2026-07-28 14:20:42 +08:00
CityFun e2754b447d feat: Add support for text-to-image APIs. (#13380)
* feat: Add support for text-to-image APIs.

* feat: Add support for text-to-image APIs.
2026-07-28 09:42:23 +08:00
331 changed files with 28471 additions and 5677 deletions
+23 -37
View File
@@ -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
[![Star History Chart](https://api.star-history.com/svg?repos=1Panel-dev/1Panel&type=Date)](https://star-history.com/#1Panel-dev/1Panel&Date)
## Community & Support ## Community & Support
+84 -2
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+6 -4
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+22 -2
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+2
View File
@@ -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
View File
@@ -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)
+32 -4
View File
@@ -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
} }
+1 -1
View File
@@ -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
+14
View File
@@ -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{}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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)
} }
+3 -2
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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)
} }
+1 -1
View File
@@ -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
} }
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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
+259
View File
@@ -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
View File
@@ -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"`
} }
+4 -2
View File
@@ -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 -1
View File
@@ -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 {
+6
View File
@@ -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"`
+1
View File
@@ -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"`
+1
View File
@@ -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"`
+1 -14
View File
@@ -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"`
} }
+43
View File
@@ -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"`
}
+1
View File
@@ -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"`
} }
+4 -2
View File
@@ -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 {
+5
View File
@@ -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 {
+4 -3
View File
@@ -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 {
+2
View File
@@ -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
+46
View File
@@ -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"`
}
+1
View File
@@ -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"`
} }
+3 -2
View File
@@ -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"`
} }
+2
View File
@@ -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:"-"`
} }
+28
View File
@@ -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
View File
@@ -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
+12 -2
View File
@@ -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
}
+3
View File
@@ -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
+48 -1
View File
@@ -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
+3 -2
View File
@@ -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
+18
View File
@@ -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 {
+3
View File
@@ -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 {
+131
View File
@@ -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
View File
@@ -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
} }
+128
View File
@@ -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
}
-53
View File
@@ -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 {
+315
View File
@@ -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)
}
}
+101 -9
View File
@@ -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) {
+24 -3
View File
@@ -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
View File
@@ -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, &paramMap); err != nil { paramMap := make(map[string]interface{})
return appDetailDTO, err if err = yaml.Unmarshal(param, &paramMap); 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)
+8 -4
View File
@@ -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
+35 -17
View File
@@ -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,
+927
View File
@@ -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
View File
@@ -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
} }
} }
+27 -2
View File
@@ -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
+33 -1
View File
@@ -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", ""))
}() }()
+41
View File
@@ -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
+3 -3
View File
@@ -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 {
+10 -4
View File
@@ -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
+10 -3
View File
@@ -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{}{}
} }
+327 -38
View File
@@ -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
+16 -10
View File
@@ -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")
+11 -5
View File
@@ -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
} }
+1
View File
@@ -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 {
+25 -5
View File
@@ -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 &currentInfo
}
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 &currentInfo return &currentInfo
} }
+12 -4
View File
@@ -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
} }
+5 -2
View File
@@ -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
+7 -3
View File
@@ -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,
}) })
+5 -2
View File
@@ -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
+69 -20
View File
@@ -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)
+47 -27
View File
@@ -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 {
+8 -6
View File
@@ -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
View File
@@ -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)
+32 -22
View File
@@ -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")
} }
+77
View File
@@ -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 -98
View File
@@ -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 {
+1 -1
View File
@@ -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 {
+235
View File
@@ -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
View File
@@ -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
+7 -2
View File
@@ -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
+5 -2
View File
@@ -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