Compare commits

..
Author SHA1 Message Date
guoyuqi 04665066dc refactor: refactor website files 2026-10-12 17:31:21 +08:00
3 changed files with 364 additions and 109 deletions
+18 -8
View File
@@ -526,10 +526,8 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
website.SiteDir = create.SiteDir
}
createdFiles := &websiteCreateFiles{}
configNginx := func(t *task.Task) error {
if err = configDefaultNginx(website, domains, appInstall, runtime, create.StreamConfig, initialSSL); err != nil {
return err
}
if create.Type == constant.Static && create.TemplateOutputID > 0 {
templateOutput, err := websiteTemplateOutputRepo.GetFirst(repo.WithByID(create.TemplateOutputID))
if err != nil {
@@ -541,12 +539,15 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
if _, err := os.Stat(templateOutput.OutputPath); err != nil {
return buserr.New("ErrFileNotFound")
}
if err := copyDir(templateOutput.OutputPath, GetSitePath(*website, SiteIndexDir)); err != nil {
if err := createdFiles.copyTemplate(templateOutput.OutputPath, GetSitePath(*website, SiteIndexDir)); err != nil {
return err
}
}
if err = configDefaultNginx(website, domains, appInstall, runtime, create.StreamConfig, initialSSL, createdFiles); err != nil {
return err
}
if website.Type != constant.Stream {
if err = createWafConfig(website, domains); err != nil {
if err = createWafConfig(website, domains, createdFiles); err != nil {
return err
}
if create.Type == constant.Runtime {
@@ -555,7 +556,9 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
return err
}
if runtime.Type == constant.RuntimePHP && runtime.Resource == constant.ResourceAppstore {
createOpenBasedirConfig(website)
if err := createdFiles.write(path.Join(GetSitePath(*website, SiteIndexDir), ".user.ini"), []byte(fmt.Sprintf("open_basedir=/www/sites/%s/index:/tmp/", website.Alias)), 0644, true); err != nil {
return err
}
}
}
}
@@ -575,12 +578,19 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
}
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
return err
}
return nil
}
deleteWebsite := func(t *task.Task) {
_ = deleteWebsiteFolder(website)
if rollbackErr := createdFiles.rollback(); rollbackErr != nil {
t.Log(fmt.Sprintf("website file rollback: %v", rollbackErr))
}
if reloadErr := opNginx(nginxInstall.ContainerName, constant.NginxReload); reloadErr != nil {
t.Log(fmt.Sprintf("reload OpenResty after rollback: %v", reloadErr))
}
}
createTask.AddSubTask(i18n.GetMsgByKey("ConfigOpenresty"), configNginx, deleteWebsite)
+300 -76
View File
@@ -1,6 +1,7 @@
package service
import (
"bytes"
"context"
"crypto/x509"
"encoding/json"
@@ -14,6 +15,7 @@ import (
"sort"
"strconv"
"strings"
"sync"
"syscall"
"time"
@@ -124,7 +126,255 @@ func applyLocationProxyPass(location *components.Location, proxyPass string, sni
location.UpdateDirective("proxy_ssl_name", []string{sslName})
}
func createIndexFile(website *model.Website, runtime *model.Runtime) error {
// websiteCreateFiles owns only paths actually created by this attempt.
// Never recursively remove a reused directory (or a newly populated one).
type websiteCreateFiles struct {
mu sync.Mutex
entries []websiteCreatedPath
rolledBack bool
restores []func() error
}
type websiteCreatedPath struct {
path string
info os.FileInfo
}
func (f *websiteCreateFiles) mkdir(name string) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.rolledBack {
return fmt.Errorf("website creation already rolled back")
}
return f.mkdirLocked(filepath.Clean(name))
}
func (f *websiteCreateFiles) mkdirLocked(name string) error {
info, err := os.Lstat(name)
if err == nil {
if !info.IsDir() {
return fmt.Errorf("website path is not a directory: %s", name)
}
return nil
}
if !os.IsNotExist(err) {
return err
}
if err = f.mkdirLocked(filepath.Dir(name)); err != nil {
return err
}
if err = os.Mkdir(name, constant.DirPerm); err != nil {
if os.IsExist(err) {
return f.mkdirLocked(name)
}
return err
}
info, err = os.Lstat(name)
if err != nil {
return err
}
f.entries = append(f.entries, websiteCreatedPath{name, info})
return nil
}
// preserveExisting is for user content and logs; generated configuration must
// fail on collision. Exclusive creation also protects against concurrent writes.
func (f *websiteCreateFiles) write(name string, data []byte, mode os.FileMode, preserveExisting bool) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.rolledBack {
return fmt.Errorf("website creation already rolled back")
}
if err := f.mkdirLocked(filepath.Dir(name)); err != nil {
return err
}
file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode)
if os.IsExist(err) && preserveExisting {
info, statErr := os.Lstat(name)
if statErr != nil {
return statErr
}
if !info.Mode().IsRegular() {
return fmt.Errorf("website path is not a regular file: %s", name)
}
return nil
}
if err != nil {
return fmt.Errorf("create website file %s: %w", name, err)
}
info, err := file.Stat()
if err != nil {
_ = file.Close()
return err
}
f.entries = append(f.entries, websiteCreatedPath{name, info})
_, err = file.Write(data)
closeErr := file.Close()
if err != nil {
return err
}
return closeErr
}
func (f *websiteCreateFiles) rollback() error {
f.mu.Lock()
defer f.mu.Unlock()
f.rolledBack = true
var firstErr error
for i := len(f.restores) - 1; i >= 0; i-- {
if err := f.restores[i](); err != nil && firstErr == nil {
firstErr = err
}
}
f.restores = nil
for i := len(f.entries) - 1; i >= 0; i-- {
entry := f.entries[i]
info, err := os.Lstat(entry.path)
if os.IsNotExist(err) {
continue
}
if err != nil {
if firstErr == nil {
firstErr = err
}
continue
}
if !os.SameFile(entry.info, info) {
continue
}
// os.Remove refuses non-empty directories, preserving untracked content.
if err = os.Remove(entry.path); err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
}
func (f *websiteCreateFiles) copyTemplate(src, dst string) error {
// Check the entire template before copying any files. Reject symlinks and
// collisions rather than truncating existing content.
err := filepath.Walk(src, func(name string, info os.FileInfo, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(src, name)
if err != nil {
return err
}
target := filepath.Join(dst, rel)
if !info.IsDir() && !info.Mode().IsRegular() {
return fmt.Errorf("unsupported template file: %s", name)
}
existing, err := os.Lstat(target)
if os.IsNotExist(err) {
return nil
}
if err != nil {
return err
}
if info.IsDir() && existing.IsDir() {
return nil
}
return fmt.Errorf("website template path already exists: %s", target)
})
if err != nil {
return err
}
return filepath.Walk(src, func(name string, info os.FileInfo, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(src, name)
if err != nil {
return err
}
target := filepath.Join(dst, rel)
if info.IsDir() {
return f.mkdir(target)
}
if !info.Mode().IsRegular() {
return fmt.Errorf("unsupported template file: %s", name)
}
data, err := os.ReadFile(name)
if err != nil {
return err
}
return f.write(target, data, constant.FilePerm, false)
})
}
// Only change ownership on files created by this attempt, never recursively on
// an existing document root.
func (f *websiteCreateFiles) chownNew(root string) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.rolledBack {
return fmt.Errorf("website creation already rolled back")
}
for _, entry := range f.entries {
rel, err := filepath.Rel(root, entry.path)
if err != nil {
return err
}
if rel == ".." || filepath.IsAbs(rel) || (len(rel) > 3 && rel[:3] == "../") {
continue
}
info, err := os.Lstat(entry.path)
if err != nil {
return err
}
if !os.SameFile(entry.info, info) {
continue
}
if err := os.Chown(entry.path, constant.WebsiteUID, constant.WebsiteGID); err != nil {
return err
}
}
return nil
}
// Shared WAF metadata is restored only while it still contains our exact write.
// Never restore a stale snapshot over changes made by another operation.
func (f *websiteCreateFiles) replaceShared(name string, before, after []byte) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.rolledBack {
return fmt.Errorf("website creation already rolled back")
}
info, err := os.Lstat(name)
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return fmt.Errorf("website config is not a regular file: %s", name)
}
current, err := os.ReadFile(name)
if err != nil {
return err
}
if !bytes.Equal(current, before) {
return fmt.Errorf("website config changed during creation: %s", name)
}
f.restores = append(f.restores, func() error {
now, err := os.Lstat(name)
if err != nil {
return err
}
if !os.SameFile(info, now) {
return fmt.Errorf("website config replaced; skipping rollback: %s", name)
}
data, err := os.ReadFile(name)
if err != nil {
return err
}
if !bytes.Equal(data, after) {
return fmt.Errorf("website config changed; skipping rollback: %s", name)
}
return os.WriteFile(name, before, info.Mode().Perm())
})
return os.WriteFile(name, after, info.Mode().Perm())
}
func createIndexFile(website *model.Website, runtime *model.Runtime, created *websiteCreateFiles) error {
var (
indexPath string
indexContent string
@@ -145,49 +395,23 @@ func createIndexFile(website *model.Website, runtime *model.Runtime) error {
}
}
fileOp := files.NewFileOp()
if !fileOp.Stat(indexFolder) {
if err := fileOp.CreateDir(indexFolder, constant.DirPerm); err != nil {
if err := created.write(indexPath, []byte(indexContent), constant.FilePerm, true); err != nil {
return err
}
}
if !fileOp.Stat(indexPath) {
if err := fileOp.CreateFile(indexPath); err != nil {
html404, _ := websiteService.GetDefaultHtml("404")
if err := created.write(path.Join(indexFolder, "404.html"), []byte(html404.Content), constant.FilePerm, true); err != nil {
return err
}
}
if website.Type == constant.Runtime && runtime.Resource == constant.ResourceAppstore {
if err := chownRootDir(indexFolder); err != nil {
return err
}
}
if err := fileOp.WriteFile(indexPath, strings.NewReader(indexContent), constant.DirPerm); err != nil {
return err
}
html404, _ := websiteService.GetDefaultHtml("404")
path404 := path.Join(indexFolder, "404.html")
if err := fileOp.WriteFile(path404, strings.NewReader(html404.Content), constant.DirPerm); err != nil {
return err
return created.chownNew(indexFolder)
}
return nil
}
func createProxyFile(website *model.Website) error {
func createProxyFile(website *model.Website, created *websiteCreateFiles) error {
proxyFolder := GetSitePath(*website, SiteProxyDir)
filePath := path.Join(proxyFolder, "root.conf")
fileOp := files.NewFileOp()
if !fileOp.Stat(proxyFolder) {
if err := fileOp.CreateDir(proxyFolder, constant.DirPerm); err != nil {
return err
}
}
if !fileOp.Stat(filePath) {
if err := fileOp.CreateFile(filePath); err != nil {
return err
}
}
config, err := parser.NewStringParser(string(nginx_conf.Proxy)).Parse()
if err != nil {
return err
@@ -201,57 +425,40 @@ func createProxyFile(website *model.Website) error {
location.ChangePath("^~", "/")
applyLocationProxyPass(location, website.Proxy, nil, "")
location.UpdateDirective("proxy_set_header", []string{"Host", "$host"})
if err := nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
if err := created.write(filePath, []byte(nginx.DumpConfig(config, nginx.IndentedStyle)), constant.FilePerm, false); err != nil {
return buserr.WithErr("ErrUpdateBuWebsite", err)
}
return nil
}
func createWebsiteFolder(website *model.Website, runtime *model.Runtime) error {
func createWebsiteFolder(website *model.Website, runtime *model.Runtime, created *websiteCreateFiles) error {
siteFolder := GetSiteDir(website.Alias)
fileOp := files.NewFileOp()
if !fileOp.Stat(siteFolder) {
if err := fileOp.CreateDir(siteFolder, constant.DirPerm); err != nil {
return err
}
if err := fileOp.CreateDir(path.Join(siteFolder, "log"), constant.DirPerm); err != nil {
return err
}
if err := fileOp.CreateFile(path.Join(siteFolder, "log", "access.log")); err != nil {
return err
}
if err := fileOp.CreateFile(path.Join(siteFolder, "log", "error.log")); err != nil {
return err
}
dirs := []string{siteFolder, path.Join(siteFolder, "log")}
if website.Type != constant.Stream {
if err := fileOp.CreateDir(path.Join(siteFolder, "index"), constant.DirPerm); err != nil {
return err
dirs = append(dirs, path.Join(siteFolder, "index"), path.Join(siteFolder, "ssl"))
}
if err := fileOp.CreateDir(path.Join(siteFolder, "ssl"), constant.DirPerm); err != nil {
for _, dir := range dirs {
if err := created.mkdir(dir); err != nil {
return err
}
}
if website.Type == constant.Runtime {
if runtime.Type == constant.RuntimePHP && runtime.Resource == constant.ResourceLocal {
phpPoolDir := path.Join(siteFolder, "php-pool")
if err := fileOp.CreateDir(phpPoolDir, constant.DirPerm); err != nil {
for _, name := range []string{"access.log", "error.log"} {
if err := created.write(path.Join(siteFolder, "log", name), nil, constant.FilePerm, true); err != nil {
return err
}
if err := fileOp.CreateFile(path.Join(phpPoolDir, "php-fpm.sock")); err != nil {
return err
}
if website.Type == constant.Runtime && runtime.Type == constant.RuntimePHP && runtime.Resource == constant.ResourceLocal {
if err := created.write(path.Join(siteFolder, "php-pool", "php-fpm.sock"), nil, constant.FilePerm, true); err != nil {
return err
}
}
if website.Type == constant.Static || (website.Type == constant.Runtime && runtime.Type == constant.RuntimePHP) {
if err := createIndexFile(website, runtime); err != nil {
if err := createIndexFile(website, runtime, created); err != nil {
return err
}
}
if website.Type == constant.Proxy {
if err := createProxyFile(website); err != nil {
return err
}
}
return createProxyFile(website, created)
}
return nil
}
@@ -261,12 +468,12 @@ type websiteInitialSSL struct {
request request.WebsiteHTTPSOp
}
func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, appInstall *model.AppInstall, runtime *model.Runtime, streamConfig request.StreamConfig, initialSSL *websiteInitialSSL) error {
func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, appInstall *model.AppInstall, runtime *model.Runtime, streamConfig request.StreamConfig, initialSSL *websiteInitialSSL, created *websiteCreateFiles) error {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
if err = createWebsiteFolder(website, runtime); err != nil {
if err = createWebsiteFolder(website, runtime, created); err != nil {
return err
}
var (
@@ -334,7 +541,10 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a
if initialSSL != nil {
plan := buildWebsiteTLSPlan(domains, nginxInstall.HttpPort, nginxInstall.HttpsPort)
applyWebsiteSSLConfig(server, *website, plan, initialSSL.request)
if err = createPemFile(*website, initialSSL.certificate); err != nil {
if err = created.write(path.Join(GetSitePath(*website, SiteSSLDir), "fullchain.pem"), []byte(initialSSL.certificate.Pem), constant.FilePerm, false); err != nil {
return err
}
if err = created.write(path.Join(GetSitePath(*website, SiteSSLDir), "privkey.pem"), []byte(initialSSL.certificate.PrivateKey), 0600, false); err != nil {
return err
}
}
@@ -404,7 +614,7 @@ func configDefaultNginx(website *model.Website, domains []model.WebsiteDomain, a
}
}
config.FilePath = configPath
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
if err = created.write(configPath, []byte(nginx.DumpConfig(config, nginx.IndentedStyle)), constant.FilePerm, false); err != nil {
return err
}
if err = opNginx(nginxInstall.ContainerName, constant.NginxCheck); err != nil {
@@ -499,7 +709,7 @@ func createOpenBasedirConfig(website *model.Website) {
_ = fileOp.SaveFile(userIniPath, fmt.Sprintf("open_basedir=/www/sites/%s/index:/tmp/", website.Alias), 0644)
}
func createWafConfig(website *model.Website, domains []model.WebsiteDomain) error {
func createWafConfig(website *model.Website, domains []model.WebsiteDomain, created *websiteCreateFiles) error {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
@@ -520,6 +730,11 @@ func createWafConfig(website *model.Website, domains []model.WebsiteDomain) erro
return err
}
}
for _, existing := range websitesArray {
if existing.Key == website.Alias {
return fmt.Errorf("WAF website already exists: %s", website.Alias)
}
}
wafWebsite := request.WafWebsite{
Key: website.Alias,
Domains: make([]string, 0),
@@ -535,9 +750,6 @@ func createWafConfig(website *model.Website, domains []model.WebsiteDomain) erro
if err != nil {
return err
}
if err := fileOp.SaveFileWithByte(websitesConfigPath, websitesContent, constant.DirPerm); err != nil {
return err
}
var (
sitesDir = path.Join(wafDataPath, "sites")
@@ -551,13 +763,25 @@ func createWafConfig(website *model.Website, domains []model.WebsiteDomain) erro
return err
}
defer func() {
if err != nil {
_ = fileOp.DeleteDir(websiteDir)
if err := created.write(path.Join(websiteDir, "config.json"), defaultConfigContent, constant.FilePerm, false); err != nil {
return err
}
}()
if err := moveDefaultWafConfig(websiteDir, defaultConfigContent, defaultRuleDir, fileOp); err != nil {
if err := created.mkdir(path.Join(websiteDir, "rules")); err != nil {
return err
}
for _, name := range []string{"acl", "args", "cookie", "defaultUaBlack", "defaultUrlBlack", "fileExt", "header", "methodWhite", "cdn"} {
data, err := os.ReadFile(path.Join(defaultRuleDir, name+".json"))
if os.IsNotExist(err) {
continue
}
if err != nil {
return err
}
if err := created.write(path.Join(websiteDir, "rules", name+".json"), data, constant.FilePerm, false); err != nil {
return err
}
}
if err := created.replaceShared(websitesConfigPath, content, websitesContent); err != nil {
return err
}
@@ -32,8 +32,13 @@
@user-change="currentUser = $event"
/>
<div class="user-actions" :class="{ 'is-collapsed': isCollapse }">
<button
<el-tooltip
v-if="currentUser"
:content="$t('xpack.user.userInfo')"
placement="top"
popper-class="sidebar-user-tooltip"
>
<button
type="button"
class="user-entry"
:aria-label="$t('xpack.user.userInfo') + ' ' + currentUser.name"
@@ -42,6 +47,8 @@
<SvgIcon class="icon" iconName="p-gerenzhongxin1" />
<span v-if="!isCollapse" class="user-name">{{ currentUser.name }}</span>
</button>
</el-tooltip>
<el-tooltip :content="$t('header.exitSystem')" placement="top" popper-class="sidebar-user-tooltip">
<button
type="button"
class="user-entry logout-entry"
@@ -50,6 +57,7 @@
>
<SvgIcon class="icon" iconName="p-tuichudenglu3" />
</button>
</el-tooltip>
</div>
</div>
</template>
@@ -433,3 +441,16 @@ watch(
border-left: 1px solid var(--panel-footer-border);
}
</style>
<style lang="scss">
.el-popper.sidebar-user-tooltip {
background: var(--el-menu-item-bg-color);
color: var(--el-color-primary);
border-color: var(--panel-footer-border);
.el-popper__arrow::before {
background: var(--el-menu-item-bg-color);
border-color: var(--panel-footer-border);
}
}
</style>