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
145 changed files with 3096 additions and 1080 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
+2 -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
+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
+2 -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
+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
+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)
+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
+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
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 {
+16 -5
View File
@@ -40,20 +40,21 @@ 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(true, "openai-completions", "openai-responses", "anthropic-messages", "openai-images"), 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(false, "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(false, "openai-completions", "openai-responses", "anthropic-messages", "openai-images"), 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"}, {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"}},
@@ -131,6 +132,10 @@ var catalog = map[string]Meta{
{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-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"}},
}, },
@@ -296,7 +301,7 @@ func DefaultModels(key, apiType string) []Model {
if len(config.Models) > 0 { if len(config.Models) > 0 {
return append([]Model(nil), config.Models...) return append([]Model(nil), config.Models...)
} }
if IsImageAPIType(config.APIType) { if IsImageAPIType(config.APIType) || IsEmbeddingAPIType(config.APIType) {
return nil return nil
} }
break break
@@ -359,7 +364,7 @@ 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) { if key == "custom" && (IsImageAPIType(config.APIType) || IsEmbeddingAPIType(config.APIType)) {
return baseURL, nil return baseURL, nil
} }
parsed.Path = normalizeEndpointPath(config.APIType, parsed.Path) parsed.Path = normalizeEndpointPath(config.APIType, parsed.Path)
@@ -381,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) {
@@ -390,6 +397,10 @@ func normalizeEndpointPath(apiType, value string) string {
return path return path
} }
func IsEmbeddingAPIType(apiType string) bool {
return apiType == "openai-embeddings"
}
func IsImageAPIType(apiType string) bool { func IsImageAPIType(apiType string) bool {
switch apiType { switch apiType {
case "openai-images", "dashscope-images", "minimax-images", "openrouter-images": case "openai-images", "dashscope-images", "minimax-images", "openrouter-images":
+1 -1
View File
@@ -23,7 +23,7 @@ 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) { if IsImageAPIType(resolvedAPIType) || IsEmbeddingAPIType(resolvedAPIType) {
return nil, fmt.Errorf("api type %s does not support text generation", 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)
+15
View File
@@ -81,6 +81,10 @@ 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": case "openai-images":
request.URL = imageVerifyURL(provider, baseURL, "/images/generations") request.URL = imageVerifyURL(provider, baseURL, "/images/generations")
headers["Authorization"] = "Bearer " + apiKey headers["Authorization"] = "Bearer " + apiKey
@@ -131,6 +135,17 @@ 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 { func imageVerifyURL(provider, baseURL, endpoint string) string {
if provider == "custom" || strings.HasSuffix(strings.ToLower(baseURL), endpoint) { if provider == "custom" || strings.HasSuffix(strings.ToLower(baseURL), endpoint) {
return baseURL return baseURL
+1 -1
View File
@@ -86,7 +86,7 @@ func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]i
Model(&model.AgentAccount{}). Model(&model.AgentAccount{}).
Select("provider, COUNT(*) as count"). Select("provider, COUNT(*) as count").
Where("provider IN ?", normalizedProviders). Where("provider IN ?", normalizedProviders).
Where("api_type NOT LIKE ?", "%-images"). Scopes(WithTextAPIType()).
Group("provider"). Group("provider").
Scan(&rows).Error; err != nil { Scan(&rows).Error; err != nil {
return nil, err return nil, err
+1 -1
View File
@@ -108,7 +108,7 @@ func WithByAPIType(apiType string) DBOption {
func WithTextAPIType() DBOption { func WithTextAPIType() DBOption {
return func(g *gorm.DB) *gorm.DB { return func(g *gorm.DB) *gorm.DB {
return g.Where("api_type NOT LIKE ?", "%-images") return g.Where("api_type NOT LIKE ? AND api_type <> ?", "%-images", "openai-embeddings")
} }
} }
+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 {
+14 -6
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)
@@ -745,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))
@@ -760,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)
} }
@@ -1128,7 +1136,7 @@ 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))
@@ -1149,7 +1157,7 @@ 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{
+1 -1
View File
@@ -116,7 +116,7 @@ func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, mode
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired") return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
} }
imageAPI := providercatalog.IsImageAPIType(resolvedAPIType) imageAPI := providercatalog.IsImageAPIType(resolvedAPIType)
if validateAvailability && (imageAPI || !providercatalog.SkipVerification(provider)) { 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
} }
+21 -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)
@@ -497,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"),
@@ -505,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,
+4 -5
View File
@@ -426,7 +426,7 @@ func (u *appUpgradeContext) cutover(t *task.Task) error {
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("Run"), i18n.GetMsgByKey("App")) logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("Run"), i18n.GetMsgByKey("App"))
t.LogStart(logStr) t.LogStart(logStr)
if out, upErr := compose.UpWithoutPull(u.original.GetComposePath()); upErr != nil { if out, upErr := compose.UpWithoutBuild(u.original.GetComposePath()); upErr != nil {
if out != "" { if out != "" {
upErr = fmt.Errorf("%s: %w", out, upErr) upErr = fmt.Errorf("%s: %w", out, upErr)
} }
@@ -513,7 +513,6 @@ func (u *appUpgradeContext) applyStagedFiles() error {
nginxModuleBuildDir, nginxModuleBuildDir,
nginxModuleModulesDir, nginxModuleModulesDir,
path.Join(nginxModuleConfDir, nginxModuleEnabledConfDir), path.Join(nginxModuleConfDir, nginxModuleEnabledConfDir),
path.Join(nginxModuleConfDir, "nginx.conf"),
} { } {
if err := replaceUpgradePath(u.stageDir, u.original.GetPath(), relativePath); err != nil { if err := replaceUpgradePath(u.stageDir, u.original.GetPath(), relativePath); err != nil {
return err return err
@@ -546,7 +545,7 @@ func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
return u.finishRollback() return u.finishRollback()
} }
if u.phase < appUpgradeMutated { if u.phase < appUpgradeMutated {
if out, err := compose.UpWithoutPull(u.original.GetComposePath()); err != nil { if out, err := compose.UpWithoutBuild(u.original.GetComposePath()); err != nil {
if out != "" { if out != "" {
err = fmt.Errorf("%s: %w", out, err) err = fmt.Errorf("%s: %w", out, err)
} }
@@ -564,14 +563,14 @@ func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
if u.backupFile != "" { if u.backupFile != "" {
_ = u.restoreManagedFiles() _ = u.restoreManagedFiles()
if err := handleAppRecover(&u.original, t, u.backupFile, true, "", ""); err != nil { if err := handleAppRecover(&u.original, t, u.backupFile, true, "", ""); err != nil {
_, _ = compose.UpWithoutPull(u.original.GetComposePath()) _, _ = compose.UpWithoutBuild(u.original.GetComposePath())
return errors.Join(rollbackErr, err) return errors.Join(rollbackErr, err)
} }
} else { } else {
if err := u.restoreManagedFiles(); err != nil { if err := u.restoreManagedFiles(); err != nil {
return errors.Join(rollbackErr, err) return errors.Join(rollbackErr, err)
} }
if out, err := compose.UpWithoutPull(u.original.GetComposePath()); err != nil { if out, err := compose.UpWithoutBuild(u.original.GetComposePath()); err != nil {
if out != "" { if out != "" {
err = fmt.Errorf("%s: %w", out, err) err = fmt.Errorf("%s: %w", out, err)
} }
+69 -4
View File
@@ -53,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 {
@@ -1387,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)
@@ -1446,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
@@ -1480,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,
@@ -1524,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))
+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
+1 -1
View File
@@ -232,7 +232,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
} }
defer func() { defer func() {
if isRollback { if isRollback {
_, _ = compose.UpWithoutPull(install.GetComposePath()) _, _ = compose.UpWithoutBuild(install.GetComposePath())
} else { } else {
_, _ = compose.Up(install.GetComposePath()) _, _ = compose.Up(install.GetComposePath())
} }
+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
+6 -3
View File
@@ -65,7 +65,7 @@ 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
@@ -77,7 +77,7 @@ type IContainerService interface {
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)
@@ -568,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
@@ -612,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))
+5 -2
View File
@@ -35,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
@@ -187,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
} }
+5 -2
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
+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
+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 {
+79 -21
View File
@@ -52,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)
@@ -72,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
@@ -84,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 (
@@ -908,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
@@ -934,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 {
@@ -951,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())
} }
@@ -970,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) {
@@ -1507,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)
@@ -1560,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())
}
+7 -3
View File
@@ -21,7 +21,7 @@ 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) CreateWebsite(req dto.FtpCreate) (uint, error)
@@ -74,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
@@ -85,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
+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)
+6 -2
View File
@@ -48,7 +48,7 @@ const (
) )
type IMcpServerService interface { type IMcpServerService interface {
Page(req request.McpServerSearch) response.McpServersRes Page(req request.McpServerSearch, readOnly ...bool) response.McpServersRes
Detail(req request.McpServerDetail) (response.McpServerDTO, error) Detail(req request.McpServerDetail) (response.McpServerDTO, error)
Create(create request.McpServerCreate) error Create(create request.McpServerCreate) error
Update(req request.McpServerUpdate) error Update(req request.McpServerUpdate) error
@@ -65,7 +65,7 @@ func NewIMcpServerService() IMcpServerService {
return &McpServerService{} return &McpServerService{}
} }
func (m McpServerService) Page(req request.McpServerSearch) response.McpServersRes { func (m McpServerService) Page(req request.McpServerSearch, readOnly ...bool) response.McpServersRes {
var ( var (
res response.McpServersRes res response.McpServersRes
items []response.McpServerDTO items []response.McpServerDTO
@@ -74,6 +74,10 @@ func (m McpServerService) Page(req request.McpServerSearch) response.McpServersR
total, data, _ := mcpServerRepo.Page(req.PageInfo.Page, req.PageInfo.PageSize) total, data, _ := mcpServerRepo.Page(req.PageInfo.Page, req.PageInfo.PageSize)
for _, item := range data { for _, item := range data {
normalizeMcpServerGateway(&item) normalizeMcpServerGateway(&item)
if isDemoReadOnly(readOnly...) {
item.DockerCompose = ""
item.Env = ""
}
items = append(items, response.McpServerDTO{ items = append(items, response.McpServerDTO{
McpServer: item, McpServer: item,
Environments: make([]request.Environment, 0), Environments: make([]request.Environment, 0),
+5
View File
@@ -155,6 +155,7 @@ func (m *MonitorService) LoadGPUOptions() dto.MonitorGPUOptions {
if (item.MaxPowerLimit == "" || item.MaxPowerLimit == "N/A") && (item.PowerDraw == "" || item.PowerDraw == "N/A") { if (item.MaxPowerLimit == "" || item.MaxPowerLimit == "N/A") && (item.PowerDraw == "" || item.PowerDraw == "N/A") {
chartHide.Power = true chartHide.Power = true
} }
chartHide.PowerLimit = item.MaxPowerLimit == "" || item.MaxPowerLimit == "N/A"
chartHide.Temperature = item.Temperature == "" || item.Temperature == "N/A" chartHide.Temperature = item.Temperature == "" || item.Temperature == "N/A"
chartHide.Speed = item.FanSpeed == "" || item.FanSpeed == "N/A" chartHide.Speed = item.FanSpeed == "" || item.FanSpeed == "N/A"
data.ChartHide = append(data.ChartHide, chartHide) data.ChartHide = append(data.ChartHide, chartHide)
@@ -174,6 +175,7 @@ func (m *MonitorService) LoadGPUOptions() dto.MonitorGPUOptions {
var chartHide dto.GPUChartHide var chartHide dto.GPUChartHide
chartHide.GPU = true chartHide.GPU = true
chartHide.Speed = true chartHide.Speed = true
chartHide.PowerLimit = true
chartHide.ProductName = fmt.Sprintf("%d - %s", item.Basic.DeviceID, item.Basic.DeviceName) chartHide.ProductName = fmt.Sprintf("%d - %s", item.Basic.DeviceID, item.Basic.DeviceName)
if (item.Stats.MemoryUsed == "" || item.Stats.MemoryUsed == "N/A") && (item.Basic.Memory == "" || item.Basic.FreeMemory == "N/A") { if (item.Stats.MemoryUsed == "" || item.Stats.MemoryUsed == "N/A") && (item.Basic.Memory == "" || item.Basic.FreeMemory == "N/A") {
chartHide.Memory = true chartHide.Memory = true
@@ -344,6 +346,7 @@ func (m *MonitorService) Run() {
_ = monitorRepo.DelMonitorBase(timeForDelete) _ = monitorRepo.DelMonitorBase(timeForDelete)
_ = monitorRepo.DelMonitorIO(timeForDelete) _ = monitorRepo.DelMonitorIO(timeForDelete)
_ = monitorRepo.DelMonitorNet(timeForDelete) _ = monitorRepo.DelMonitorNet(timeForDelete)
_ = monitorRepo.DelMonitorGPU(timeForDelete)
} }
func (m *MonitorService) loadDiskIO() { func (m *MonitorService) loadDiskIO() {
@@ -599,6 +602,7 @@ func saveGPUDataToDB() {
} }
gpuInfo, err := client.LoadGpuInfo() gpuInfo, err := client.LoadGpuInfo()
if err != nil { if err != nil {
global.LOG.Errorf("load gpu monitor data failed, err: %v", err)
return return
} }
var list []model.MonitorGPU var list []model.MonitorGPU
@@ -631,6 +635,7 @@ func saveXPUDataToDB() {
} }
xpuInfo, err := client.LoadGpuInfo() xpuInfo, err := client.LoadGpuInfo()
if err != nil { if err != nil {
global.LOG.Errorf("load xpu monitor data failed, err: %v", err)
return return
} }
var list []model.MonitorGPU var list []model.MonitorGPU
+21 -7
View File
@@ -48,7 +48,7 @@ type IRuntimeService interface {
Create(create request.RuntimeCreate) (*model.Runtime, error) Create(create request.RuntimeCreate) (*model.Runtime, error)
Delete(delete request.RuntimeDelete) error Delete(delete request.RuntimeDelete) error
Update(req request.RuntimeUpdate) error Update(req request.RuntimeUpdate) error
Get(id uint) (res *response.RuntimeDTO, err error) Get(id uint, readOnly ...bool) (res *response.RuntimeDTO, err error)
GetNodePackageRunScript(req request.NodePackageReq) ([]response.PackageScripts, error) GetNodePackageRunScript(req request.NodePackageReq) ([]response.PackageScripts, error)
OperateRuntime(req request.RuntimeOperate) error OperateRuntime(req request.RuntimeOperate) error
GetNodeModules(req request.NodeModuleReq) ([]response.NodeModule, error) GetNodeModules(req request.NodeModuleReq) ([]response.NodeModule, error)
@@ -70,7 +70,7 @@ type IRuntimeService interface {
GetFPMConfig(id uint) (*request.FPMConfig, error) GetFPMConfig(id uint) (*request.FPMConfig, error)
UpdatePHPContainer(req request.PHPContainerConfig) error UpdatePHPContainer(req request.PHPContainerConfig) error
GetPHPContainerConfig(id uint) (*request.PHPContainerConfig, error) GetPHPContainerConfig(id uint, readOnly ...bool) (*request.PHPContainerConfig, error)
GetSupervisorProcess(id uint) ([]response.SupervisorProcessConfig, error) GetSupervisorProcess(id uint) ([]response.SupervisorProcessConfig, error)
OperateSupervisorProcess(req request.PHPSupervisorProcessConfig) error OperateSupervisorProcess(req request.PHPSupervisorProcessConfig) error
@@ -332,8 +332,11 @@ func (r *RuntimeService) Page(req request.RuntimeSearch) (int64, []response.Runt
if len(runtimes) == 0 { if len(runtimes) == 0 {
return 0, res, nil return 0, res, nil
} }
if err = SyncRuntimesStatus(runtimes); err != nil { readOnlyMode := isDemoReadOnly(req.ReadOnly)
return 0, nil, err if !readOnlyMode {
if err = SyncRuntimesStatus(runtimes); err != nil {
return 0, nil, err
}
} }
for _, runtime := range runtimes { for _, runtime := range runtimes {
if runtime.Resource == constant.ResourceLocal { if runtime.Resource == constant.ResourceLocal {
@@ -347,7 +350,7 @@ func (r *RuntimeService) Page(req request.RuntimeSearch) (int64, []response.Runt
} }
detail, _ := appDetailRepo.GetFirst(repo.WithByID(runtime.AppDetailID)) detail, _ := appDetailRepo.GetFirst(repo.WithByID(runtime.AppDetailID))
if detail.AppId == 0 { if detail.AppId == 0 {
appID, appDetailID := handleRuntimeDetailID(runtime) appID, appDetailID := handleRuntimeDetailID(runtime, !readOnlyMode)
runtimeDTO.AppDetailID = appDetailID runtimeDTO.AppDetailID = appDetailID
runtimeDTO.AppID = appID runtimeDTO.AppID = appID
} else { } else {
@@ -358,6 +361,9 @@ func (r *RuntimeService) Page(req request.RuntimeSearch) (int64, []response.Runt
runtimeDTO.Params[k] = v runtimeDTO.Params[k] = v
} }
} }
if readOnlyMode {
redactSensitiveValues(runtimeDTO.Params)
}
runtimeDTO.ExposedPorts, _ = loadComposeExposedPortsFromEnv(envs, "", false) runtimeDTO.ExposedPorts, _ = loadComposeExposedPortsFromEnv(envs, "", false)
res = append(res, runtimeDTO) res = append(res, runtimeDTO)
} }
@@ -538,7 +544,7 @@ func deleteRuntimeImages(runtime *model.Runtime, taskItem *task.Task) {
} }
} }
func (r *RuntimeService) Get(id uint) (*response.RuntimeDTO, error) { func (r *RuntimeService) Get(id uint, readOnly ...bool) (*response.RuntimeDTO, error) {
runtime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(id)) runtime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(id))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -627,6 +633,11 @@ func (r *RuntimeService) Get(id uint) (*response.RuntimeDTO, error) {
return nil, err return nil, err
} }
} }
if isDemoReadOnly(readOnly...) {
redactSensitiveValues(res.Params)
redactSensitiveAppParams(res.AppParams)
redactSensitiveEnvironments(res.Environments)
}
return &res, nil return &res, nil
} }
@@ -1348,7 +1359,7 @@ func (r *RuntimeService) UpdatePHPContainer(req request.PHPContainerConfig) erro
return nil return nil
} }
func (r *RuntimeService) GetPHPContainerConfig(id uint) (*request.PHPContainerConfig, error) { func (r *RuntimeService) GetPHPContainerConfig(id uint, readOnly ...bool) (*request.PHPContainerConfig, error) {
runtime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(id)) runtime, err := runtimeRepo.GetFirst(context.Background(), repo.WithByID(id))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1365,6 +1376,9 @@ func (r *RuntimeService) GetPHPContainerConfig(id uint) (*request.PHPContainerCo
Volumes: runtimeDTO.Volumes, Volumes: runtimeDTO.Volumes,
ExtraHosts: runtimeDTO.ExtraHosts, ExtraHosts: runtimeDTO.ExtraHosts,
} }
if isDemoReadOnly(readOnly...) {
redactSensitiveEnvironments(res.Environments)
}
return res, nil return res, nil
} }
+10 -3
View File
@@ -245,7 +245,9 @@ func SyncRuntimesStatus(runtimes []model.Runtime) error {
case "restarting": case "restarting":
runtimes[index].Status = constant.StatusRestarting runtimes[index].Status = constant.StatusRestarting
} }
_ = runtimeRepo.Save(&runtimes[index]) if !global.CONF.Base.IsDemo {
_ = runtimeRepo.Save(&runtimes[index])
}
delete(runtimeContainer, contain.Names[0]) delete(runtimeContainer, contain.Names[0])
} }
} }
@@ -295,6 +297,9 @@ func SyncRuntimeContainerStatus(runtime *model.Runtime) error {
} }
} }
if global.CONF.Base.IsDemo {
return nil
}
return runtimeRepo.Save(runtime) return runtimeRepo.Save(runtime)
} }
@@ -1148,7 +1153,7 @@ func getOperation(operate, pkgManager string) string {
return ops[1] return ops[1]
} }
func handleRuntimeDetailID(runtime model.Runtime) (uint, uint) { func handleRuntimeDetailID(runtime model.Runtime, persist bool) (uint, uint) {
app, _ := appRepo.GetFirst(appRepo.WithKey(runtime.Type)) app, _ := appRepo.GetFirst(appRepo.WithKey(runtime.Type))
if app.ID == 0 { if app.ID == 0 {
return 0, 0 return 0, 0
@@ -1158,6 +1163,8 @@ func handleRuntimeDetailID(runtime model.Runtime) (uint, uint) {
return 0, 0 return 0, 0
} }
runtime.AppDetailID = appDetail.ID runtime.AppDetailID = appDetail.ID
_ = runtimeRepo.Save(&runtime) if persist && !global.CONF.Base.IsDemo {
_ = runtimeRepo.Save(&runtime)
}
return app.ID, appDetail.ID return app.ID, appDetail.ID
} }
+8 -2
View File
@@ -40,7 +40,7 @@ type ISettingService interface {
SaveConnInfo(req dto.SSHConnData) error SaveConnInfo(req dto.SSHConnData) error
SetDefaultIsConn(req dto.SSHDefaultConn) error SetDefaultIsConn(req dto.SSHDefaultConn) error
GetSystemProxy() (*dto.SystemProxy, error) GetSystemProxy() (*dto.SystemProxy, error)
GetLocalConn() dto.SSHConnData GetLocalConn(readOnly ...bool) dto.SSHConnData
GetLocalConnForSSH() (dto.SSHConnData, error) GetLocalConnForSSH() (dto.SSHConnData, error)
SaveDescription(req dto.CommonDescription) error SaveDescription(req dto.CommonDescription) error
@@ -311,8 +311,14 @@ func (u *SettingService) loadLocalConn() dto.SSHConnData {
return data return data
} }
func (u *SettingService) GetLocalConn() dto.SSHConnData { func (u *SettingService) GetLocalConn(readOnly ...bool) dto.SSHConnData {
data := u.loadLocalConn() data := u.loadLocalConn()
if isDemoReadOnly(readOnly...) {
data.Password = ""
data.PrivateKey = ""
data.PassPhrase = ""
return data
}
if len(data.Password) != 0 { if len(data.Password) != 0 {
data.Password = base64.StdEncoding.EncodeToString([]byte(data.Password)) data.Password = base64.StdEncoding.EncodeToString([]byte(data.Password))
} }
+52 -10
View File
@@ -57,7 +57,7 @@ type ISSHService interface {
SyncRootCert() error SyncRootCert() error
CreateRootCert(req dto.RootCertOperate) error CreateRootCert(req dto.RootCertOperate) error
EditRootCert(req dto.RootCertOperate) error EditRootCert(req dto.RootCertOperate) error
SearchRootCerts(req dto.SearchWithPage) (int64, interface{}, error) SearchRootCerts(req dto.SearchWithPage, readOnly ...bool) (int64, interface{}, error)
DeleteRootCerts(req dto.ForceDelete) error DeleteRootCerts(req dto.ForceDelete) error
} }
@@ -585,7 +585,7 @@ func (u *SSHService) EditRootCert(req dto.RootCertOperate) error {
return hostRepo.SaveCert(&cert) return hostRepo.SaveCert(&cert)
} }
func (u *SSHService) SearchRootCerts(req dto.SearchWithPage) (int64, interface{}, error) { func (u *SSHService) SearchRootCerts(req dto.SearchWithPage, readOnly ...bool) (int64, interface{}, error) {
total, records, err := hostRepo.PageCert(req.Page, req.PageSize) total, records, err := hostRepo.PageCert(req.Page, req.PageSize)
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
@@ -597,12 +597,15 @@ func (u *SSHService) SearchRootCerts(req dto.SearchWithPage) (int64, interface{}
if err == nil && len(publicItem) != 0 { if err == nil && len(publicItem) != 0 {
publicBase64 = base64.StdEncoding.EncodeToString(publicItem) publicBase64 = base64.StdEncoding.EncodeToString(publicItem)
} }
privateItem, _ := os.ReadFile(records[i].PrivateKeyPath)
var privateBase64 string var privateBase64 string
if err == nil && len(publicItem) != 0 { var passPhrase string
privateBase64 = base64.StdEncoding.EncodeToString(privateItem) if !isDemoReadOnly(readOnly...) {
privateItem, _ := os.ReadFile(records[i].PrivateKeyPath)
if len(privateItem) != 0 {
privateBase64 = base64.StdEncoding.EncodeToString(privateItem)
}
passPhrase, _ = encrypt.StringDecryptWithBase64(records[i].PassPhrase)
} }
passPhrase, _ := encrypt.StringDecryptWithBase64(records[i].PassPhrase)
datas = append(datas, dto.RootCert{ datas = append(datas, dto.RootCert{
ID: records[i].ID, ID: records[i].ID,
CreatedAt: records[i].CreatedAt, CreatedAt: records[i].CreatedAt,
@@ -1328,6 +1331,8 @@ func collectSSHLogItems(lines []string, filter, status string) []sshParsedLog {
var items []sshParsedLog var items []sshParsedLog
auxiliaryIndex := make(map[string]int) auxiliaryIndex := make(map[string]int)
sessionHasAuthEvent := make(map[string]bool) sessionHasAuthEvent := make(map[string]bool)
authenticatedEndpoints := make(map[string]bool)
matchedTerminalSessions := make(map[string]bool)
for lineIndex, line := range lines { for lineIndex, line := range lines {
if !shouldParseSSHLogLine(line, status, filter) { if !shouldParseSSHLogLine(line, status, filter) {
continue continue
@@ -1338,11 +1343,32 @@ func collectSSHLogItems(lines []string, filter, status string) []sshParsedLog {
} }
item.Index = lineIndex item.Index = lineIndex
item.Search = line item.Search = line
endpointKey := loadSSHLogEndpointKey(item.History)
if isSSHAuthEvent(item) && item.SessionKey != "" { if isSSHAuthEvent(item) && item.SessionKey != "" {
sessionHasAuthEvent[item.SessionKey] = true sessionHasAuthEvent[item.SessionKey] = true
delete(matchedTerminalSessions, item.SessionKey)
if endpointKey != "" {
if item.History.Status == constant.StatusSuccess {
authenticatedEndpoints[endpointKey] = true
} else {
delete(authenticatedEndpoints, endpointKey)
}
}
items = append(items, item) items = append(items, item)
continue continue
} }
if endpointKey != "" && isSSHTerminalEvent(item) && authenticatedEndpoints[endpointKey] {
// A successful authentication and its terminal log can be emitted by
// different sshd processes. Associate only this active connection by
// endpoint, then clear it so a reused client port starts a new session.
delete(authenticatedEndpoints, endpointKey)
matchedTerminalSessions[item.SessionKey] = true
continue
}
if isSSHTerminalEvent(item) && matchedTerminalSessions[item.SessionKey] {
continue
}
delete(matchedTerminalSessions, item.SessionKey)
if index, ok := auxiliaryIndex[item.SessionKey]; ok { if index, ok := auxiliaryIndex[item.SessionKey]; ok {
items[index].Search += "\n" + line items[index].Search += "\n" + line
continue continue
@@ -1381,6 +1407,13 @@ func isSSHAuthEvent(item sshParsedLog) bool {
return item.History.Status == constant.StatusSuccess || item.History.AuthMode != "" return item.History.Status == constant.StatusSuccess || item.History.AuthMode != ""
} }
func isSSHTerminalEvent(item sshParsedLog) bool {
message := item.History.Message
return strings.HasPrefix(message, "Connection closed by ") ||
strings.HasPrefix(message, "Disconnected from ") ||
strings.HasPrefix(message, "Received disconnect from ")
}
func shouldParseSSHLogLine(line, status, filter string) bool { func shouldParseSSHLogLine(line, status, filter string) bool {
if !strings.Contains(line, "sshd") { if !strings.Contains(line, "sshd") {
return false return false
@@ -1482,18 +1515,27 @@ func parseSSHLogHeader(line string) (sshLogHeader, bool) {
} }
func loadSSHLogSessionKey(header sshLogHeader, data dto.SSHHistory, line string) string { func loadSSHLogSessionKey(header sshLogHeader, data dto.SSHHistory, line string) string {
if header.Process == "sshd-session" && data.Address != "" && data.Port != "" { if header.Process == "sshd-session" {
return data.Address + ":" + data.Port if endpointKey := loadSSHLogEndpointKey(data); endpointKey != "" {
return endpointKey
}
} }
if header.PID != "" { if header.PID != "" {
return "pid:" + header.PID return "pid:" + header.PID
} }
if data.Address != "" && data.Port != "" { if endpointKey := loadSSHLogEndpointKey(data); endpointKey != "" {
return data.Address + ":" + data.Port return endpointKey
} }
return line return line
} }
func loadSSHLogEndpointKey(data dto.SSHHistory) string {
if data.Address == "" || data.Port == "" {
return ""
}
return net.JoinHostPort(data.Address, data.Port)
}
func normalizeSSHLogDate(dateStr string) string { func normalizeSSHLogDate(dateStr string) string {
if t, err := time.Parse(time.RFC3339Nano, dateStr); err == nil { if t, err := time.Parse(time.RFC3339Nano, dateStr); err == nil {
return t.Format("2006 Jan 2 15:04:05") return t.Format("2006 Jan 2 15:04:05")
+19 -5
View File
@@ -28,7 +28,7 @@ import (
type TensorRTLLMService struct{} type TensorRTLLMService struct{}
type ITensorRTLLMService interface { type ITensorRTLLMService interface {
Page(req request.TensorRTLLMSearch) response.TensorRTLLMsRes Page(req request.TensorRTLLMSearch, readOnly bool) response.TensorRTLLMsRes
Create(create request.TensorRTLLMCreate) error Create(create request.TensorRTLLMCreate) error
Update(req request.TensorRTLLMUpdate) error Update(req request.TensorRTLLMUpdate) error
Delete(id uint) error Delete(id uint) error
@@ -39,18 +39,23 @@ func NewITensorRTLLMService() ITensorRTLLMService {
return &TensorRTLLMService{} return &TensorRTLLMService{}
} }
func (t TensorRTLLMService) Page(req request.TensorRTLLMSearch) response.TensorRTLLMsRes { func (t TensorRTLLMService) Page(req request.TensorRTLLMSearch, readOnly bool) response.TensorRTLLMsRes {
var ( var (
res response.TensorRTLLMsRes res response.TensorRTLLMsRes
items []response.TensorRTLLMDTO items []response.TensorRTLLMDTO
) )
readOnlyMode := isDemoReadOnly(readOnly)
total, data, _ := tensorrtLLMRepo.Page(req.PageInfo.Page, req.PageInfo.PageSize) total, data, _ := tensorrtLLMRepo.Page(req.PageInfo.Page, req.PageInfo.PageSize)
for _, item := range data { for _, item := range data {
_ = syncTensorRTLLMContainerStatus(&item) _ = syncTensorRTLLMContainerStatus(&item, readOnlyMode)
serverDTO := response.TensorRTLLMDTO{ serverDTO := response.TensorRTLLMDTO{
TensorRTLLM: item, TensorRTLLM: item,
} }
if readOnlyMode {
serverDTO.DockerCompose = ""
serverDTO.Env = ""
}
envs, _ := gotenv.Unmarshal(item.Env) envs, _ := gotenv.Unmarshal(item.Env)
serverDTO.Version = envs["VERSION"] serverDTO.Version = envs["VERSION"]
serverDTO.ModelDir = envs["MODEL_PATH"] serverDTO.ModelDir = envs["MODEL_PATH"]
@@ -63,6 +68,9 @@ func (t TensorRTLLMService) Page(req request.TensorRTLLMSearch) response.TensorR
composeByte, err := files.NewFileOp().GetContent(path.Join(global.Dir.TensorRTLLMDir, item.Name, "docker-compose.yml")) composeByte, err := files.NewFileOp().GetContent(path.Join(global.Dir.TensorRTLLMDir, item.Name, "docker-compose.yml"))
if err == nil { if err == nil {
serverDTO.Environments, _ = getDockerComposeEnvironments(composeByte) serverDTO.Environments, _ = getDockerComposeEnvironments(composeByte)
if readOnlyMode {
redactSensitiveEnvironments(serverDTO.Environments)
}
} }
volumes, err := getDockerComposeVolumes(composeByte) volumes, err := getDockerComposeVolumes(composeByte)
if err == nil { if err == nil {
@@ -323,10 +331,10 @@ func startTensorRTLLM(tensorrtLLM *model.TensorRTLLM) {
tensorrtLLM.Status = constant.StatusRunning tensorrtLLM.Status = constant.StatusRunning
tensorrtLLM.Message = "" tensorrtLLM.Message = ""
} }
_ = syncTensorRTLLMContainerStatus(tensorrtLLM) _ = syncTensorRTLLMContainerStatus(tensorrtLLM, false)
} }
func syncTensorRTLLMContainerStatus(tensorrtLLM *model.TensorRTLLM) error { func syncTensorRTLLMContainerStatus(tensorrtLLM *model.TensorRTLLM, readOnly bool) error {
containerNames := []string{tensorrtLLM.ContainerName} containerNames := []string{tensorrtLLM.ContainerName}
cli, err := docker.NewClient() cli, err := docker.NewClient()
if err != nil { if err != nil {
@@ -342,6 +350,9 @@ func syncTensorRTLLMContainerStatus(tensorrtLLM *model.TensorRTLLM) error {
return nil return nil
} }
tensorrtLLM.Status = constant.StatusStopped tensorrtLLM.Status = constant.StatusStopped
if readOnly || global.CONF.Base.IsDemo {
return nil
}
return tensorrtLLMRepo.Save(tensorrtLLM) return tensorrtLLMRepo.Save(tensorrtLLM)
} }
container := containers[0] container := containers[0]
@@ -359,6 +370,9 @@ func syncTensorRTLLMContainerStatus(tensorrtLLM *model.TensorRTLLM) error {
tensorrtLLM.Status = constant.StatusStopped tensorrtLLM.Status = constant.StatusStopped
} }
} }
if readOnly || global.CONF.Base.IsDemo {
return nil
}
return tensorrtLLMRepo.Save(tensorrtLLM) return tensorrtLLMRepo.Save(tensorrtLLM)
} }
+13 -4
View File
@@ -94,7 +94,7 @@ type IWebsiteService interface {
GetWebsiteNginxConfig(websiteId uint, configType string) (*response.FileInfo, error) GetWebsiteNginxConfig(websiteId uint, configType string) (*response.FileInfo, error)
UpdateNginxConfigFile(req request.WebsiteNginxUpdate) error UpdateNginxConfigFile(req request.WebsiteNginxUpdate) error
GetWebsiteHTTPS(websiteId uint) (response.WebsiteHTTPS, error) GetWebsiteHTTPS(websiteId uint, readOnly ...bool) (response.WebsiteHTTPS, error)
OpWebsiteHTTPS(ctx context.Context, req request.WebsiteHTTPSOp) (*response.WebsiteHTTPS, error) OpWebsiteHTTPS(ctx context.Context, req request.WebsiteHTTPSOp) (*response.WebsiteHTTPS, error)
LoadWebsiteDirConfig(req request.WebsiteCommonReq) (*response.WebsiteDirConfig, error) LoadWebsiteDirConfig(req request.WebsiteCommonReq) (*response.WebsiteDirConfig, error)
@@ -117,7 +117,7 @@ type IWebsiteService interface {
SetRealIPConfig(req request.WebsiteRealIP) error SetRealIPConfig(req request.WebsiteRealIP) error
GetRealIPConfig(websiteID uint) (*response.WebsiteRealIP, error) GetRealIPConfig(websiteID uint) (*response.WebsiteRealIP, error)
GetWebsiteResource(websiteID uint) ([]response.Resource, error) GetWebsiteResource(websiteID uint, readOnly ...bool) ([]response.Resource, error)
ListDatabases() ([]response.Database, error) ListDatabases() ([]response.Database, error)
ChangeDatabase(req request.ChangeDatabase) error ChangeDatabase(req request.ChangeDatabase) error
@@ -914,7 +914,7 @@ func (w WebsiteService) GetWebsiteNginxConfig(websiteID uint, configType string)
return &response.FileInfo{FileInfo: *info}, nil return &response.FileInfo{FileInfo: *info}, nil
} }
func (w WebsiteService) GetWebsiteHTTPS(websiteId uint) (response.WebsiteHTTPS, error) { func (w WebsiteService) GetWebsiteHTTPS(websiteId uint, readOnly ...bool) (response.WebsiteHTTPS, error) {
website, err := websiteRepo.GetFirst(repo.WithByID(websiteId)) website, err := websiteRepo.GetFirst(repo.WithByID(websiteId))
if err != nil { if err != nil {
return response.WebsiteHTTPS{}, err return response.WebsiteHTTPS{}, err
@@ -938,6 +938,10 @@ func (w WebsiteService) GetWebsiteHTTPS(websiteId uint) (response.WebsiteHTTPS,
return response.WebsiteHTTPS{}, err return response.WebsiteHTTPS{}, err
} }
res.SSL = *websiteSSL res.SSL = *websiteSSL
if isDemoReadOnly(readOnly...) {
res.SSL.PrivateKey = ""
res.SSL.AcmeAccount.EabHmacKey = ""
}
res.Enable = true res.Enable = true
if website.HttpConfig != "" { if website.HttpConfig != "" {
res.HttpConfig = website.HttpConfig res.HttpConfig = website.HttpConfig
@@ -2238,7 +2242,7 @@ func (w WebsiteService) GetRealIPConfig(websiteID uint) (*response.WebsiteRealIP
return res, err return res, err
} }
func (w WebsiteService) GetWebsiteResource(websiteID uint) ([]response.Resource, error) { func (w WebsiteService) GetWebsiteResource(websiteID uint, readOnly ...bool) ([]response.Resource, error) {
website, err := websiteRepo.GetFirst(repo.WithByID(websiteID)) website, err := websiteRepo.GetFirst(repo.WithByID(websiteID))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -2307,6 +2311,11 @@ func (w WebsiteService) GetWebsiteResource(websiteID uint) ([]response.Resource,
} }
} }
} }
if isDemoReadOnly(readOnly...) {
for i := range res {
res[i].Detail = nil
}
}
return res, nil return res, nil
} }
+5 -2
View File
@@ -14,7 +14,7 @@ type WebsiteAcmeAccountService struct {
} }
type IWebsiteAcmeAccountService interface { type IWebsiteAcmeAccountService interface {
Page(search dto.PageInfo) (int64, []response.WebsiteAcmeAccountDTO, error) Page(search dto.PageInfo, readOnly ...bool) (int64, []response.WebsiteAcmeAccountDTO, error)
Create(create request.WebsiteAcmeAccountCreate) (*response.WebsiteAcmeAccountDTO, error) Create(create request.WebsiteAcmeAccountCreate) (*response.WebsiteAcmeAccountDTO, error)
Delete(id uint) error Delete(id uint) error
Update(update request.WebsiteAcmeAccountUpdate) (*response.WebsiteAcmeAccountDTO, error) Update(update request.WebsiteAcmeAccountUpdate) (*response.WebsiteAcmeAccountDTO, error)
@@ -24,10 +24,13 @@ func NewIWebsiteAcmeAccountService() IWebsiteAcmeAccountService {
return &WebsiteAcmeAccountService{} return &WebsiteAcmeAccountService{}
} }
func (w WebsiteAcmeAccountService) Page(search dto.PageInfo) (int64, []response.WebsiteAcmeAccountDTO, error) { func (w WebsiteAcmeAccountService) Page(search dto.PageInfo, readOnly ...bool) (int64, []response.WebsiteAcmeAccountDTO, error) {
total, accounts, err := websiteAcmeRepo.Page(search.Page, search.PageSize, repo.WithOrderDesc("created_at")) total, accounts, err := websiteAcmeRepo.Page(search.Page, search.PageSize, repo.WithOrderDesc("created_at"))
var accountDTOs []response.WebsiteAcmeAccountDTO var accountDTOs []response.WebsiteAcmeAccountDTO
for _, account := range accounts { for _, account := range accounts {
if isDemoReadOnly(readOnly...) {
account.EabHmacKey = ""
}
accountDTOs = append(accountDTOs, response.WebsiteAcmeAccountDTO{ accountDTOs = append(accountDTOs, response.WebsiteAcmeAccountDTO{
WebsiteAcmeAccount: account, WebsiteAcmeAccount: account,
}) })
+10 -4
View File
@@ -37,9 +37,9 @@ type WebsiteCAService struct {
} }
type IWebsiteCAService interface { type IWebsiteCAService interface {
Page(search request.WebsiteCASearch) (int64, []response.WebsiteCADTO, error) Page(search request.WebsiteCASearch, readOnly ...bool) (int64, []response.WebsiteCADTO, error)
Create(create request.WebsiteCACreate) (*request.WebsiteCACreate, error) Create(create request.WebsiteCACreate) (*request.WebsiteCACreate, error)
GetCA(id uint) (*response.WebsiteCADTO, error) GetCA(id uint, readOnly ...bool) (*response.WebsiteCADTO, error)
Delete(id uint) error Delete(id uint) error
ObtainSSL(req request.WebsiteCAObtain) (*model.WebsiteSSL, error) ObtainSSL(req request.WebsiteCAObtain) (*model.WebsiteSSL, error)
DownloadFile(id uint) (*os.File, error) DownloadFile(id uint) (*os.File, error)
@@ -49,13 +49,16 @@ func NewIWebsiteCAService() IWebsiteCAService {
return &WebsiteCAService{} return &WebsiteCAService{}
} }
func (w WebsiteCAService) Page(search request.WebsiteCASearch) (int64, []response.WebsiteCADTO, error) { func (w WebsiteCAService) Page(search request.WebsiteCASearch, readOnly ...bool) (int64, []response.WebsiteCADTO, error) {
total, cas, err := websiteCARepo.Page(search.Page, search.PageSize, repo.WithOrderDesc("created_at")) total, cas, err := websiteCARepo.Page(search.Page, search.PageSize, repo.WithOrderDesc("created_at"))
if err != nil { if err != nil {
return 0, nil, err return 0, nil, err
} }
var caDTOs []response.WebsiteCADTO var caDTOs []response.WebsiteCADTO
for _, ca := range cas { for _, ca := range cas {
if isDemoReadOnly(readOnly...) {
ca.PrivateKey = ""
}
caDTOs = append(caDTOs, response.WebsiteCADTO{ caDTOs = append(caDTOs, response.WebsiteCADTO{
WebsiteCA: ca, WebsiteCA: ca,
}) })
@@ -125,12 +128,15 @@ func (w WebsiteCAService) Create(create request.WebsiteCACreate) (*request.Websi
return &create, nil return &create, nil
} }
func (w WebsiteCAService) GetCA(id uint) (*response.WebsiteCADTO, error) { func (w WebsiteCAService) GetCA(id uint, readOnly ...bool) (*response.WebsiteCADTO, error) {
res := &response.WebsiteCADTO{} res := &response.WebsiteCADTO{}
ca, err := websiteCARepo.GetFirst(repo.WithByID(id)) ca, err := websiteCARepo.GetFirst(repo.WithByID(id))
if err != nil { if err != nil {
return nil, err return nil, err
} }
if isDemoReadOnly(readOnly...) {
ca.PrivateKey = ""
}
res.WebsiteCA = ca res.WebsiteCA = ca
certBlock, _ := pem.Decode([]byte(ca.CSR)) certBlock, _ := pem.Decode([]byte(ca.CSR))
if certBlock == nil { if certBlock == nil {
+5 -3
View File
@@ -16,7 +16,7 @@ type WebsiteDnsAccountService struct {
} }
type IWebsiteDnsAccountService interface { type IWebsiteDnsAccountService interface {
Page(search dto.PageInfo) (int64, []response.WebsiteDnsAccountDTO, error) Page(search dto.PageInfo, readOnly ...bool) (int64, []response.WebsiteDnsAccountDTO, error)
Create(create request.WebsiteDnsAccountCreate) (request.WebsiteDnsAccountCreate, error) Create(create request.WebsiteDnsAccountCreate) (request.WebsiteDnsAccountCreate, error)
Update(update request.WebsiteDnsAccountUpdate) (request.WebsiteDnsAccountUpdate, error) Update(update request.WebsiteDnsAccountUpdate) (request.WebsiteDnsAccountUpdate, error)
Delete(id uint) error Delete(id uint) error
@@ -26,12 +26,14 @@ func NewIWebsiteDnsAccountService() IWebsiteDnsAccountService {
return &WebsiteDnsAccountService{} return &WebsiteDnsAccountService{}
} }
func (w WebsiteDnsAccountService) Page(search dto.PageInfo) (int64, []response.WebsiteDnsAccountDTO, error) { func (w WebsiteDnsAccountService) Page(search dto.PageInfo, readOnly ...bool) (int64, []response.WebsiteDnsAccountDTO, error) {
total, accounts, err := websiteDnsRepo.Page(search.Page, search.PageSize, repo.WithOrderDesc("created_at")) total, accounts, err := websiteDnsRepo.Page(search.Page, search.PageSize, repo.WithOrderDesc("created_at"))
var accountDTOs []response.WebsiteDnsAccountDTO var accountDTOs []response.WebsiteDnsAccountDTO
for _, account := range accounts { for _, account := range accounts {
auth := make(map[string]string) auth := make(map[string]string)
_ = json.Unmarshal([]byte(account.Authorization), &auth) if !isDemoReadOnly(readOnly...) {
_ = json.Unmarshal([]byte(account.Authorization), &auth)
}
accountDTOs = append(accountDTOs, response.WebsiteDnsAccountDTO{ accountDTOs = append(accountDTOs, response.WebsiteDnsAccountDTO{
WebsiteDnsAccount: account, WebsiteDnsAccount: account,
Authorization: auth, Authorization: auth,
+18 -6
View File
@@ -95,12 +95,12 @@ func newWebsiteSSLLegoClient(ctx context.Context, acmeAccount *model.WebsiteAcme
} }
type IWebsiteSSLService interface { type IWebsiteSSLService interface {
Page(search request.WebsiteSSLSearch) (int64, []response.WebsiteSSLDTO, error) Page(search request.WebsiteSSLSearch, readOnly ...bool) (int64, []response.WebsiteSSLDTO, error)
GetSSL(id uint) (*response.WebsiteSSLDTO, error) GetSSL(id uint, readOnly ...bool) (*response.WebsiteSSLDTO, error)
Search(req request.WebsiteSSLListReq) ([]response.WebsiteSSLDTO, error) Search(req request.WebsiteSSLListReq) ([]response.WebsiteSSLDTO, error)
Create(create request.WebsiteSSLCreate) (request.WebsiteSSLCreate, error) Create(create request.WebsiteSSLCreate) (request.WebsiteSSLCreate, error)
GetDNSResolve(req request.WebsiteDNSReq) ([]response.WebsiteDNSRes, error) GetDNSResolve(req request.WebsiteDNSReq) ([]response.WebsiteDNSRes, error)
GetWebsiteSSL(websiteId uint) (response.WebsiteSSLDTO, error) GetWebsiteSSL(websiteId uint, readOnly ...bool) (response.WebsiteSSLDTO, error)
Delete(ids []uint) error Delete(ids []uint) error
Update(update request.WebsiteSSLUpdate) error Update(update request.WebsiteSSLUpdate) error
Upload(req request.WebsiteSSLUpload) error Upload(req request.WebsiteSSLUpload) error
@@ -116,7 +116,7 @@ func NewIWebsiteSSLService() IWebsiteSSLService {
return &WebsiteSSLService{} return &WebsiteSSLService{}
} }
func (w WebsiteSSLService) Page(search request.WebsiteSSLSearch) (int64, []response.WebsiteSSLDTO, error) { func (w WebsiteSSLService) Page(search request.WebsiteSSLSearch, readOnly ...bool) (int64, []response.WebsiteSSLDTO, error) {
var ( var (
result []response.WebsiteSSLDTO result []response.WebsiteSSLDTO
opts []repo.DBOption opts []repo.DBOption
@@ -134,6 +134,10 @@ func (w WebsiteSSLService) Page(search request.WebsiteSSLSearch) (int64, []respo
return 0, nil, err return 0, nil, err
} }
for _, model := range sslList { for _, model := range sslList {
if isDemoReadOnly(readOnly...) {
model.PrivateKey = ""
model.AcmeAccount.EabHmacKey = ""
}
result = append(result, response.WebsiteSSLDTO{ result = append(result, response.WebsiteSSLDTO{
WebsiteSSL: model, WebsiteSSL: model,
LogPath: path.Join(global.Dir.SSLLogDir, fmt.Sprintf("%s-ssl-%d.log", model.PrimaryDomain, model.ID)), LogPath: path.Join(global.Dir.SSLLogDir, fmt.Sprintf("%s-ssl-%d.log", model.PrimaryDomain, model.ID)),
@@ -142,12 +146,16 @@ func (w WebsiteSSLService) Page(search request.WebsiteSSLSearch) (int64, []respo
return total, result, err return total, result, err
} }
func (w WebsiteSSLService) GetSSL(id uint) (*response.WebsiteSSLDTO, error) { func (w WebsiteSSLService) GetSSL(id uint, readOnly ...bool) (*response.WebsiteSSLDTO, error) {
var res response.WebsiteSSLDTO var res response.WebsiteSSLDTO
websiteSSL, err := websiteSSLRepo.GetFirst(repo.WithByID(id)) websiteSSL, err := websiteSSLRepo.GetFirst(repo.WithByID(id))
if err != nil { if err != nil {
return nil, err return nil, err
} }
if isDemoReadOnly(readOnly...) {
websiteSSL.PrivateKey = ""
websiteSSL.AcmeAccount.EabHmacKey = ""
}
res.WebsiteSSL = *websiteSSL res.WebsiteSSL = *websiteSSL
return &res, nil return &res, nil
} }
@@ -682,7 +690,7 @@ func (w WebsiteSSLService) GetDNSResolve(req request.WebsiteDNSReq) ([]response.
return res, nil return res, nil
} }
func (w WebsiteSSLService) GetWebsiteSSL(websiteId uint) (response.WebsiteSSLDTO, error) { func (w WebsiteSSLService) GetWebsiteSSL(websiteId uint, readOnly ...bool) (response.WebsiteSSLDTO, error) {
var res response.WebsiteSSLDTO var res response.WebsiteSSLDTO
website, err := websiteRepo.GetFirst(repo.WithByID(websiteId)) website, err := websiteRepo.GetFirst(repo.WithByID(websiteId))
if err != nil { if err != nil {
@@ -692,6 +700,10 @@ func (w WebsiteSSLService) GetWebsiteSSL(websiteId uint) (response.WebsiteSSLDTO
if err != nil { if err != nil {
return res, err return res, err
} }
if isDemoReadOnly(readOnly...) {
websiteSSL.PrivateKey = ""
websiteSSL.AcmeAccount.EabHmacKey = ""
}
res.WebsiteSSL = *websiteSSL res.WebsiteSSL = *websiteSSL
return res, nil return res, nil
} }
+1
View File
@@ -22,6 +22,7 @@ const (
TypeComposeCreate = "compose-create" TypeComposeCreate = "compose-create"
InterruptedMsg = "the task was interrupted due to the restart of the 1panel service" InterruptedMsg = "the task was interrupted due to the restart of the 1panel service"
DemoModeHeader = "X-Panel-Demo-Mode"
) )
const ( const (
+2 -2
View File
@@ -10,7 +10,7 @@ require (
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/compose-spec/compose-go/v2 v2.14.0 github.com/compose-spec/compose-go/v2 v2.14.0
github.com/creack/pty v1.1.24 github.com/creack/pty v1.1.24
github.com/docker/cli v29.7.1+incompatible github.com/docker/cli v29.7.2+incompatible
github.com/docker/docker v28.5.2+incompatible github.com/docker/docker v28.5.2+incompatible
github.com/docker/go-connections v0.8.1 github.com/docker/go-connections v0.8.1
github.com/fsnotify/fsnotify v1.10.1 github.com/fsnotify/fsnotify v1.10.1
@@ -28,7 +28,7 @@ require (
github.com/jackc/pgx/v5 v5.10.0 github.com/jackc/pgx/v5 v5.10.0
github.com/jinzhu/copier v0.4.0 github.com/jinzhu/copier v0.4.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/klauspost/compress v1.19.1 github.com/klauspost/compress v1.19.2
github.com/mattn/go-shellwords v1.0.14 github.com/mattn/go-shellwords v1.0.14
github.com/mholt/archiver/v4 v4.0.0-alpha.8 github.com/mholt/archiver/v4 v4.0.0-alpha.8
github.com/miekg/dns v1.1.72 github.com/miekg/dns v1.1.72
+4 -4
View File
@@ -228,8 +228,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/cli v29.7.1+incompatible h1:ILZpP6B7fedIr6ANy824QkDp1WMJuouIq0O2SrBkB2w= github.com/docker/cli v29.7.2+incompatible h1:dlkwallR8XqfeVnA2ELEhdwvb4lsSwuB4IgsG8Q9cLY=
github.com/docker/cli v29.7.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v29.7.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY= github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
@@ -523,8 +523,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
+1
View File
@@ -44,6 +44,7 @@ func (f *FileRouter) InitRouter(Router *gin.RouterGroup) {
fileRouter.POST("/wget", baseApi.WgetFile) fileRouter.POST("/wget", baseApi.WgetFile)
fileRouter.POST("/wget/stop", baseApi.StopWget) fileRouter.POST("/wget/stop", baseApi.StopWget)
fileRouter.POST("/move", baseApi.MoveFile) fileRouter.POST("/move", baseApi.MoveFile)
fileRouter.POST("/move/stop", baseApi.StopMoveFile)
fileRouter.GET("/download", baseApi.Download) fileRouter.GET("/download", baseApi.Download)
fileRouter.POST("/share/search", baseApi.SearchFileShare) fileRouter.POST("/share/search", baseApi.SearchFileShare)
fileRouter.POST("/share/detail", baseApi.GetFileShareDetail) fileRouter.POST("/share/detail", baseApi.GetFileShareDetail)
+164
View File
@@ -0,0 +1,164 @@
package gpu
import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu/common"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)
var (
ascendVersionPattern = regexp.MustCompile(`(?i)\bVersion:\s*([^\s|]+)`)
ascendMemoryPattern = regexp.MustCompile(`([0-9]+(?:\.[0-9]+)?)\s*/\s*([0-9]+(?:\.[0-9]+)?)`)
)
type AscendSMI struct{}
func (a AscendSMI) LoadGpuInfo() (*common.GpuInfo, error) {
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(5 * time.Second))
itemData, err := cmdMgr.RunWithStdout("npu-smi", "info")
if err != nil {
return nil, fmt.Errorf("calling npu-smi failed, %v", err)
}
return parseAscendSMI(itemData), nil
}
func parseAscendSMI(data string) *common.GpuInfo {
info := &common.GpuInfo{Type: "ascend"}
if match := ascendVersionPattern.FindStringSubmatch(data); len(match) == 2 {
info.DriverVersion = match[1]
}
processSection := false
deviceIndexes := make(map[uint]int)
var pending *common.GPU
for _, line := range strings.Split(data, "\n") {
if strings.Contains(line, "Process id") && strings.Contains(line, "Process memory") {
processSection = true
pending = nil
continue
}
cells := ascendTableCells(line)
if processSection {
if len(cells) != 4 && len(cells) != 5 {
continue
}
deviceFields := strings.Fields(cells[0])
if len(deviceFields) == 0 {
continue
}
deviceID, err := strconv.ParseUint(deviceFields[0], 10, 64)
if err != nil {
continue
}
index, ok := deviceIndexes[uint(deviceID)]
if !ok {
continue
}
processOffset := len(cells) - 3
info.GPUs[index].Processes = append(info.GPUs[index].Processes, common.Process{
Pid: strings.TrimSpace(cells[processOffset]),
Type: "NPU",
ProcessName: strings.TrimSpace(cells[processOffset+1]),
UsedMemory: ascendValueWithUnit(cells[processOffset+2], "MB"),
})
continue
}
if len(cells) != 3 {
continue
}
if pending == nil {
fields := strings.Fields(cells[0])
if len(fields) < 2 {
continue
}
deviceID, err := strconv.ParseUint(fields[0], 10, 64)
if err != nil {
continue
}
metrics := strings.Fields(cells[2])
if len(metrics) < 2 {
continue
}
pending = &common.GPU{
Type: "ascend",
Index: uint(deviceID),
ProductName: strings.Join(fields[1:], " "),
PersistenceMode: "N/A",
DisplayActive: "N/A",
ECC: "N/A",
FanSpeed: "N/A",
Temperature: ascendValueWithUnit(metrics[1], "C"),
PerformanceState: strings.TrimSpace(cells[1]),
PowerDraw: ascendValueWithUnit(metrics[0], "W"),
MaxPowerLimit: "N/A",
ComputeMode: "N/A",
MigMode: "N/A",
}
continue
}
metrics := strings.Fields(cells[2])
if len(metrics) == 0 {
pending = nil
continue
}
pending.BusID = strings.TrimSpace(cells[1])
pending.GPUUtil = ascendValueWithUnit(metrics[0], "%")
pending.MemUsed, pending.MemTotal = ascendMemoryUsage(cells[2])
deviceIndexes[pending.Index] = len(info.GPUs)
info.GPUs = append(info.GPUs, *pending)
pending = nil
}
return info
}
func ascendTableCells(line string) []string {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, "|") || !strings.HasSuffix(line, "|") {
return nil
}
parts := strings.Split(line, "|")
if len(parts) < 3 {
return nil
}
cells := make([]string, 0, len(parts)-2)
for _, part := range parts[1 : len(parts)-1] {
cells = append(cells, strings.TrimSpace(part))
}
return cells
}
func ascendMemoryUsage(value string) (string, string) {
matches := ascendMemoryPattern.FindAllStringSubmatch(value, -1)
if len(matches) == 0 {
return "N/A", "N/A"
}
// 910B exposes both generic memory and HBM. Prefer the last memory pool
// with a non-zero capacity, which selects HBM while retaining compatibility
// with devices that only expose Memory-Usage.
selected := matches[len(matches)-1]
for i := len(matches) - 1; i >= 0; i-- {
if total, err := strconv.ParseFloat(matches[i][2], 64); err == nil && total > 0 {
selected = matches[i]
break
}
}
return selected[1] + " MB", selected[2] + " MB"
}
func ascendValueWithUnit(value, unit string) string {
value = strings.TrimSpace(value)
if value == "" || strings.EqualFold(value, "N/A") || strings.EqualFold(value, "NA") {
return "N/A"
}
return value + " " + unit
}
@@ -9,6 +9,7 @@ type GpuInfo struct {
} }
type GPU struct { type GPU struct {
Type string `json:"type"`
Index uint `json:"index"` Index uint `json:"index"`
ProductName string `json:"productName"` ProductName string `json:"productName"`
PersistenceMode string `json:"persistenceMode"` PersistenceMode string `json:"persistenceMode"`
+90 -2
View File
@@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"io" "io"
"strings" "strings"
"sync"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
@@ -18,8 +19,95 @@ import (
type NvidiaSMI struct{} type NvidiaSMI struct{}
func New() (bool, NvidiaSMI) { type SMI interface {
return cmd.Which("nvidia-smi"), NvidiaSMI{} LoadGpuInfo() (*common.GpuInfo, error)
}
func New() (bool, SMI) {
var clients []SMI
if cmd.Which("nvidia-smi") {
clients = append(clients, NvidiaSMI{})
}
if cmd.Which("npu-smi") {
clients = append(clients, AscendSMI{})
}
if len(clients) == 0 {
return false, nil
}
if len(clients) == 1 {
return true, clients[0]
}
return true, multiSMI{clients: clients}
}
type multiSMI struct {
clients []SMI
}
type smiResult struct {
info *common.GpuInfo
err error
}
func (m multiSMI) LoadGpuInfo() (*common.GpuInfo, error) {
results := make([]smiResult, len(m.clients))
var wg sync.WaitGroup
for index, client := range m.clients {
wg.Add(1)
go func() {
defer wg.Done()
results[index].info, results[index].err = client.LoadGpuInfo()
}()
}
wg.Wait()
merged := &common.GpuInfo{}
var (
errs []error
types []string
driverVersions []string
)
for _, result := range results {
if result.err != nil {
errs = append(errs, result.err)
continue
}
if result.info == nil {
continue
}
deviceType := result.info.Type
if deviceType != "" {
types = append(types, deviceType)
}
if result.info.DriverVersion != "" {
driverVersions = append(driverVersions, fmt.Sprintf("%s: %s", strings.ToUpper(deviceType), result.info.DriverVersion))
}
if result.info.CudaVersion != "" {
merged.CudaVersion = result.info.CudaVersion
}
for _, device := range result.info.GPUs {
if device.Type == "" {
device.Type = deviceType
}
merged.GPUs = append(merged.GPUs, device)
}
}
if len(types) == 1 {
merged.Type = types[0]
} else if len(types) > 1 {
merged.Type = "mixed"
}
if len(driverVersions) == 1 {
parts := strings.SplitN(driverVersions[0], ": ", 2)
merged.DriverVersion = parts[len(parts)-1]
} else {
merged.DriverVersion = strings.Join(driverVersions, ";")
}
if len(merged.GPUs) == 0 && len(errs) > 0 {
return nil, fmt.Errorf("calling GPU monitoring tools failed: %w", errors.Join(errs...))
}
return merged, nil
} }
func (n NvidiaSMI) LoadGpuInfo() (*common.GpuInfo, error) { func (n NvidiaSMI) LoadGpuInfo() (*common.GpuInfo, error) {
@@ -23,6 +23,7 @@ func Parse(buf []byte, version string) (*common.GpuInfo, error) {
} }
for i := 0; i < len(s.Gpu); i++ { for i := 0; i < len(s.Gpu); i++ {
var gpuItem common.GPU var gpuItem common.GPU
gpuItem.Type = "nvidia"
gpuItem.Index = uint(i) gpuItem.Index = uint(i)
gpuItem.ProductName = s.Gpu[i].ProductName gpuItem.ProductName = s.Gpu[i].ProductName
gpuItem.PersistenceMode = s.Gpu[i].PersistenceMode gpuItem.PersistenceMode = s.Gpu[i].PersistenceMode
+4 -4
View File
@@ -48,7 +48,7 @@ func Up(filePath string, projectName ...string) (string, error) {
return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Minute)).RunWithStdout(base, args...) return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Minute)).RunWithStdout(base, args...)
} }
func UpWithoutPull(filePath string, projectName ...string) (string, error) { func UpWithoutBuild(filePath string, projectName ...string) (string, error) {
if err := checkCmd(); err != nil { if err := checkCmd(); err != nil {
return "", err return "", err
} }
@@ -58,11 +58,11 @@ func UpWithoutPull(filePath string, projectName ...string) (string, error) {
return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Minute)).RunWithStdout(base, args...) return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Minute)).RunWithStdout(base, args...)
} }
func upArgs(filePath string, withoutPull bool) []string { func upArgs(filePath string, withoutBuild bool) []string {
args := loadFiles(filePath) args := loadFiles(filePath)
args = append(args, "up", "-d") args = append(args, "up", "-d")
if withoutPull { if withoutBuild {
args = append(args, "--pull", "never", "--no-build") args = append(args, "--no-build")
} }
return args return args
} }
+10 -36
View File
@@ -1,14 +1,15 @@
package docker package docker
import ( import (
"bufio"
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"path" "path"
"strings" "strings"
"github.com/compose-spec/compose-go/v2/dotenv"
"github.com/compose-spec/compose-go/v2/loader" "github.com/compose-spec/compose-go/v2/loader"
"github.com/compose-spec/compose-go/v2/template"
"github.com/compose-spec/compose-go/v2/types" "github.com/compose-spec/compose-go/v2/types"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@@ -91,7 +92,7 @@ func (e *Environment) UnmarshalYAML(value *yaml.Node) error {
} }
func GetImagesFromDockerCompose(env, yml []byte) ([]string, error) { func GetImagesFromDockerCompose(env, yml []byte) ([]string, error) {
envVars, err := loadEnvFile(env) envVars, err := dotenv.Parse(bytes.NewReader(env))
if err != nil { if err != nil {
return nil, fmt.Errorf("load env failed: %v", err) return nil, fmt.Errorf("load env failed: %v", err)
} }
@@ -104,43 +105,16 @@ func GetImagesFromDockerCompose(env, yml []byte) ([]string, error) {
var images []string var images []string
for _, service := range compose.Services { for _, service := range compose.Services {
if service.Image != "" { if service.Image != "" {
resolvedImage := replaceEnvVars(service.Image, envVars) resolvedImage, err := template.Substitute(service.Image, func(key string) (string, bool) {
value, ok := envVars[key]
return value, ok
})
if err != nil {
return nil, fmt.Errorf("resolve image failed: %v", err)
}
images = append(images, resolvedImage) images = append(images, resolvedImage)
} }
} }
return images, nil return images, nil
} }
func loadEnvFile(env []byte) (map[string]string, error) {
envVars := make(map[string]string)
scanner := bufio.NewScanner(bytes.NewReader(env))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
value = strings.Trim(value, `"'`)
envVars[key] = value
}
}
return envVars, scanner.Err()
}
func replaceEnvVars(input string, envVars map[string]string) string {
return re.GetRegex(re.ComposeEnvVarPattern).ReplaceAllStringFunc(input, func(match string) string {
varName := match[2 : len(match)-1]
if value, exists := envVars[varName]; exists {
return value
}
return match
})
}
+7 -1
View File
@@ -594,6 +594,12 @@ func (f FileOp) DownloadFile(url, dst string) error {
} }
func (f FileOp) Cut(oldPaths []string, dst, name string, cover bool) error { func (f FileOp) Cut(oldPaths []string, dst, name string, cover bool) error {
ctx, cancel := context.WithTimeout(context.Background(), cmdRecursiveTimeout)
defer cancel()
return f.CutWithContext(ctx, oldPaths, dst, name, cover)
}
func (f FileOp) CutWithContext(ctx context.Context, oldPaths []string, dst, name string, cover bool) error {
if len(oldPaths) == 0 { if len(oldPaths) == 0 {
return nil return nil
} }
@@ -617,7 +623,7 @@ func (f FileOp) Cut(oldPaths []string, dst, name string, cover bool) error {
} }
args = append(args, oldPaths...) args = append(args, oldPaths...)
args = append(args, dstPath) args = append(args, dstPath)
if err := cmd.NewCommandMgr(cmd.WithTimeout(cmdRecursiveTimeout)).Run("mv", args...); err != nil { if err := cmd.NewCommandMgr(cmd.WithContext(ctx)).Run("mv", args...); err != nil {
return err return err
} }
return nil return nil
+4 -2
View File
@@ -49,8 +49,10 @@ func Login(c *gin.Context, info dto.Login, entrance string) (*dto.UserLoginInfo,
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
if err = settingRepo.Update("Language", info.Language); err != nil { if !global.CONF.Base.IsDemo {
return nil, "", err if err = settingRepo.Update("Language", info.Language); err != nil {
return nil, "", err
}
} }
if mfaSetting.Value == constant.StatusEnable { if mfaSetting.Value == constant.StatusEnable {
return BeginMFALogin(c, nameSetting.Value, entrance, mfaSetting.Value), "", nil return BeginMFALogin(c, nameSetting.Value, entrance, mfaSetting.Value), "", nil
+16 -5
View File
@@ -99,13 +99,17 @@ func (u *SettingService) GetSettingInfo() (*dto.SettingInfo, error) {
} }
if info.Edition == "" { if info.Edition == "" {
info.Edition = "cn" info.Edition = "cn"
_ = settingRepo.UpdateOrCreate("Edition", info.Edition) if !global.CONF.Base.IsDemo {
_ = settingRepo.UpdateOrCreate("Edition", info.Edition)
}
} }
if info.MenuAccordion == "" { if info.MenuAccordion == "" {
info.MenuAccordion = constant.StatusDisable info.MenuAccordion = constant.StatusDisable
_ = settingRepo.UpdateOrCreate("MenuAccordion", info.MenuAccordion) if !global.CONF.Base.IsDemo {
_ = settingRepo.UpdateOrCreate("MenuAccordion", info.MenuAccordion)
}
} }
if info.ProxyPasswdKeep != constant.StatusEnable { if global.CONF.Base.IsDemo || info.ProxyPasswdKeep != constant.StatusEnable {
info.ProxyPasswd = "" info.ProxyPasswd = ""
} else { } else {
info.ProxyPasswd, _ = encrypt.StringDecrypt(info.ProxyPasswd) info.ProxyPasswd, _ = encrypt.StringDecrypt(info.ProxyPasswd)
@@ -142,11 +146,15 @@ func (u *SettingService) GetSettingBaseInfo() (*dto.SettingBaseInfo, error) {
} }
if info.Edition == "" { if info.Edition == "" {
info.Edition = "cn" info.Edition = "cn"
_ = settingRepo.UpdateOrCreate("Edition", info.Edition) if !global.CONF.Base.IsDemo {
_ = settingRepo.UpdateOrCreate("Edition", info.Edition)
}
} }
if info.MenuAccordion == "" { if info.MenuAccordion == "" {
info.MenuAccordion = constant.StatusDisable info.MenuAccordion = constant.StatusDisable
_ = settingRepo.UpdateOrCreate("MenuAccordion", info.MenuAccordion) if !global.CONF.Base.IsDemo {
_ = settingRepo.UpdateOrCreate("MenuAccordion", info.MenuAccordion)
}
} }
return &info, err return &info, err
@@ -516,6 +524,9 @@ func (u *SettingService) LoadFromCert() (*dto.SSLInfo, error) {
data.SSLID = uint(id) data.SSLID = uint(id)
data.Timeout = ssl.ExpireDate.Format(constant.DateTimeLayout) data.Timeout = ssl.ExpireDate.Format(constant.DateTimeLayout)
} }
if global.CONF.Base.IsDemo {
data.Key = ""
}
return &data, nil return &data, nil
} }
+2
View File
@@ -13,6 +13,7 @@ const (
DefaultDate = "1970-01-01" DefaultDate = "1970-01-01"
DateTimeLayout = "2006-01-02 15:04:05" // or use time.DateTime while go version >= 1.20 DateTimeLayout = "2006-01-02 15:04:05" // or use time.DateTime while go version >= 1.20
DateTimeSlimLayout = "20060102150405" DateTimeSlimLayout = "20060102150405"
DemoModeHeader = "X-Panel-Demo-Mode"
OrderDesc = "descending" OrderDesc = "descending"
OrderAsc = "ascending" OrderAsc = "ascending"
@@ -59,6 +60,7 @@ var WebUrlMap = map[string]struct{}{
"/ai": {}, "/ai": {},
"/ai/ai-proxy": {}, "/ai/ai-proxy": {},
"/ai/ai-proxy/overview": {},
"/ai/ai-proxy/model-pool": {}, "/ai/ai-proxy/model-pool": {},
"/ai/ai-proxy/api-keys": {}, "/ai/ai-proxy/api-keys": {},
"/ai/ai-proxy/groups": {}, "/ai/ai-proxy/groups": {},
+1 -1
View File
@@ -29,7 +29,7 @@ require (
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.11 github.com/pkg/sftp v1.13.11
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/russellhaering/goxmldsig v1.6.0 github.com/russellhaering/goxmldsig v1.6.1
github.com/shirou/gopsutil/v4 v4.26.7 github.com/shirou/gopsutil/v4 v4.26.7
github.com/sirupsen/logrus v1.9.4 github.com/sirupsen/logrus v1.9.4
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
+2 -2
View File
@@ -234,8 +234,8 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russellhaering/goxmldsig v1.6.0 h1:8fdWXEPh2k/NZNQBPFNoVfS3JmzS4ZprY/sAOpKQLks= github.com/russellhaering/goxmldsig v1.6.1 h1:SB7R5ttvrGIDB2juJAK/i7DQ2Ivr7agG+ohfNJjwyYU=
github.com/russellhaering/goxmldsig v1.6.0/go.mod h1:TrnaquDcYxWXfJrOjeMBTX4mLBeYAqaHEyUeWPxZlBM= github.com/russellhaering/goxmldsig v1.6.1/go.mod h1:haZkRcLs9W/Xp989fIjP3BrTdbFQveRF0QNZSYoH09w=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
+1 -1
View File
@@ -443,7 +443,7 @@ UpdatePortFirewallReload: "Failed to reload the firewall, {{ .err }}, manually r
# mobile app # mobile app
ErrVerifyToken: 'Token verification failed. Reset and scan again.' ErrVerifyToken: 'Token verification failed. Reset and scan again.'
ErrInvalidToken: 'Invalid token. Reset and scan again.' ErrInvalidToken: 'Invalid token. Reset and scan again.'
ErrExpiredToken: 'Token expired. Reset and scan again.' ErrExpiredToken: 'QR code is no longer valid. Generate a new one and scan again.'
# command export # command export
Name: "Name" Name: "Name"
+1 -1
View File
@@ -443,7 +443,7 @@ UpdatePortFirewallReload: "Fallo al recargar el firewall, {{ .err }}, recargue e
# aplicación móvil # aplicación móvil
ErrVerifyToken: 'Error al verificar el token, por favor restablezca y escanee de nuevo.' ErrVerifyToken: 'Error al verificar el token, por favor restablezca y escanee de nuevo.'
ErrInvalidToken: 'Token inválido, por favor restablezca y escanee de nuevo.' ErrInvalidToken: 'Token inválido, por favor restablezca y escanee de nuevo.'
ErrExpiredToken: 'El token ha expirado, por favor restablezca y escanee de nuevo.' ErrExpiredToken: 'El código QR ya no es válido. Genere uno nuevo y vuelva a escanearlo.'
# exportación de comandos # exportación de comandos
Name: "Nombre" Name: "Nombre"
+1 -1
View File
@@ -443,7 +443,7 @@ UpdatePortFirewallReload: "بارگذاری مجدد دیواره آتش نام
# برنامه موبایل # برنامه موبایل
ErrVerifyToken: 'تأیید توکن ناموفق بود. بازنشانی کرده و دوباره اسکن کنید.' ErrVerifyToken: 'تأیید توکن ناموفق بود. بازنشانی کرده و دوباره اسکن کنید.'
ErrInvalidToken: 'توکن نامعتبر است. بازنشانی کرده و دوباره اسکن کنید.' ErrInvalidToken: 'توکن نامعتبر است. بازنشانی کرده و دوباره اسکن کنید.'
ErrExpiredToken: 'توکن منقضی شده است. بازنشانی کرده و دوباره اسکن کنید.' ErrExpiredToken: 'کد QR دیگر معتبر نیست. یک کد جدید ایجاد و دوباره اسکن کنید.'
# خروجی فرمان # خروجی فرمان
Name: "نام" Name: "نام"
+1 -1
View File
@@ -443,7 +443,7 @@ UpdatePortFirewallReload: "ファイアウォールのリロードに失敗し
# モバイルアプリ # モバイルアプリ
ErrVerifyToken: 'トークンの検証エラーです。リセット後、再度QRコードをスキャンしてください。' ErrVerifyToken: 'トークンの検証エラーです。リセット後、再度QRコードをスキャンしてください。'
ErrInvalidToken: '無効なトークンです。リセット後、再度QRコードをスキャンしてください。' ErrInvalidToken: '無効なトークンです。リセット後、再度QRコードをスキャンしてください。'
ErrExpiredToken: 'トークンの有効期限が切れました。リセット後、再度QRコードをスキャンしてください。' ErrExpiredToken: 'QRコードが無効になりました。再生成してからもう一度スキャンしてください。'
# コマンドエクスポート # コマンドエクスポート
Name: "名前" Name: "名前"

Some files were not shown because too many files have changed in this diff Show More