初始化
This commit is contained in:
634
server/service/backup_service.go
Normal file
634
server/service/backup_service.go
Normal file
@@ -0,0 +1,634 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"seeyon-filesystem/config"
|
||||
"seeyon-filesystem/dto"
|
||||
"seeyon-filesystem/model"
|
||||
)
|
||||
|
||||
// BackupService 备份服务
|
||||
type BackupService struct{}
|
||||
|
||||
func NewBackupService() *BackupService {
|
||||
return &BackupService{}
|
||||
}
|
||||
|
||||
// ========== 策略管理 ==========
|
||||
|
||||
// CreatePolicy 创建备份策略
|
||||
func (s *BackupService) CreatePolicy(req *dto.BackupPolicyCreateRequest) error {
|
||||
targetConfig := req.TargetConfig
|
||||
if targetConfig == "" {
|
||||
targetConfig = "{}"
|
||||
}
|
||||
|
||||
policy := &model.BackupPolicy{
|
||||
Name: req.Name,
|
||||
BackupType: req.BackupType,
|
||||
TargetType: req.TargetType,
|
||||
TargetConfig: targetConfig,
|
||||
IncludeDB: req.IncludeDB,
|
||||
IncludeFiles: req.IncludeFiles,
|
||||
CronExpr: req.CronExpr,
|
||||
MaxBackups: req.MaxBackups,
|
||||
Status: 1,
|
||||
}
|
||||
if policy.MaxBackups <= 0 {
|
||||
policy.MaxBackups = 7
|
||||
}
|
||||
return config.DB.Create(policy).Error
|
||||
}
|
||||
|
||||
// ListPolicies 获取所有备份策略
|
||||
func (s *BackupService) ListPolicies() ([]dto.BackupPolicyVO, error) {
|
||||
var policies []model.BackupPolicy
|
||||
if err := config.DB.Order("created_at DESC").Find(&policies).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vos := make([]dto.BackupPolicyVO, 0, len(policies))
|
||||
for _, p := range policies {
|
||||
vo := dto.BackupPolicyVO{
|
||||
ID: p.ID,
|
||||
Name: p.Name,
|
||||
BackupType: p.BackupType,
|
||||
TargetType: p.TargetType,
|
||||
TargetConfig: p.TargetConfig,
|
||||
IncludeDB: p.IncludeDB,
|
||||
IncludeFiles: p.IncludeFiles,
|
||||
CronExpr: p.CronExpr,
|
||||
MaxBackups: p.MaxBackups,
|
||||
Status: p.Status,
|
||||
CreatedAt: p.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
if p.LastRunAt != nil {
|
||||
t := p.LastRunAt.Format("2006-01-02 15:04:05")
|
||||
vo.LastRunAt = &t
|
||||
}
|
||||
if p.NextRunAt != nil {
|
||||
t := p.NextRunAt.Format("2006-01-02 15:04:05")
|
||||
vo.NextRunAt = &t
|
||||
}
|
||||
vos = append(vos, vo)
|
||||
}
|
||||
return vos, nil
|
||||
}
|
||||
|
||||
// DeletePolicy 删除备份策略
|
||||
func (s *BackupService) DeletePolicy(id uint64) error {
|
||||
return config.DB.Delete(&model.BackupPolicy{}, id).Error
|
||||
}
|
||||
|
||||
// TogglePolicy 启用/禁用备份策略
|
||||
func (s *BackupService) TogglePolicy(id uint64, status int8) error {
|
||||
return config.DB.Model(&model.BackupPolicy{}).Where("id = ?", id).Update("status", status).Error
|
||||
}
|
||||
|
||||
// ========== 执行备份 ==========
|
||||
|
||||
// ExecuteBackup 手动执行备份
|
||||
func (s *BackupService) ExecuteBackup(policyID uint64) (*model.BackupLog, error) {
|
||||
var policy model.BackupPolicy
|
||||
if err := config.DB.First(&policy, policyID).Error; err != nil {
|
||||
return nil, fmt.Errorf("备份策略不存在")
|
||||
}
|
||||
|
||||
// 创建备份日志
|
||||
log := &model.BackupLog{
|
||||
PolicyID: policyID,
|
||||
PolicyName: policy.Name,
|
||||
BackupType: policy.BackupType,
|
||||
Status: "running",
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
config.DB.Create(log)
|
||||
|
||||
// 异步执行备份
|
||||
go s.doBackup(&policy, log)
|
||||
|
||||
return log, nil
|
||||
}
|
||||
|
||||
// doBackup 执行实际备份
|
||||
func (s *BackupService) doBackup(policy *model.BackupPolicy, log *model.BackupLog) {
|
||||
startTime := time.Now()
|
||||
|
||||
// 创建备份目录
|
||||
backupDir := filepath.Join(config.AppConfig.Storage.LocalBasePath, ".backups")
|
||||
os.MkdirAll(backupDir, 0755)
|
||||
|
||||
timestamp := time.Now().Format("20060102_150405")
|
||||
backupFileName := fmt.Sprintf("backup_%s_%s.tar.gz", policy.BackupType, timestamp)
|
||||
backupPath := filepath.Join(backupDir, backupFileName)
|
||||
|
||||
// 创建tar.gz文件
|
||||
tarFile, err := os.Create(backupPath)
|
||||
if err != nil {
|
||||
s.finishLog(log, "failed", "", 0, 0, err.Error())
|
||||
return
|
||||
}
|
||||
defer tarFile.Close()
|
||||
|
||||
gzWriter := gzip.NewWriter(tarFile)
|
||||
defer gzWriter.Close()
|
||||
|
||||
tarWriter := tar.NewWriter(gzWriter)
|
||||
defer tarWriter.Close()
|
||||
|
||||
totalSize := int64(0)
|
||||
filesCount := 0
|
||||
|
||||
// 备份数据库
|
||||
if policy.IncludeDB == 1 {
|
||||
dbSize, err := s.backupDatabase(tarWriter, timestamp)
|
||||
if err != nil {
|
||||
s.finishLog(log, "failed", "", 0, 0, "数据库备份失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
totalSize += dbSize
|
||||
}
|
||||
|
||||
// 备份文件
|
||||
if policy.IncludeFiles == 1 {
|
||||
fileSize, count, err := s.backupFiles(tarWriter)
|
||||
if err != nil {
|
||||
s.finishLog(log, "failed", "", 0, 0, "文件备份失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
totalSize += fileSize
|
||||
filesCount = count
|
||||
}
|
||||
|
||||
tarWriter.Close()
|
||||
gzWriter.Close()
|
||||
tarFile.Close()
|
||||
|
||||
// 获取备份文件大小
|
||||
backupInfo, _ := os.Stat(backupPath)
|
||||
backupSize := int64(0)
|
||||
if backupInfo != nil {
|
||||
backupSize = backupInfo.Size()
|
||||
}
|
||||
|
||||
duration := int(time.Since(startTime).Seconds())
|
||||
|
||||
// 更新策略最后执行时间
|
||||
now := time.Now()
|
||||
config.DB.Model(policy).Updates(map[string]interface{}{
|
||||
"last_run_at": now,
|
||||
})
|
||||
|
||||
s.finishLog(log, "success", backupPath, backupSize, filesCount, "")
|
||||
log.DBSize = totalSize
|
||||
log.Duration = duration
|
||||
config.DB.Save(log)
|
||||
|
||||
// 清理旧备份
|
||||
s.cleanOldBackups(policy)
|
||||
}
|
||||
|
||||
// backupDatabase 备份数据库
|
||||
func (s *BackupService) backupDatabase(tarWriter *tar.Writer, timestamp string) (int64, error) {
|
||||
dbCfg := config.AppConfig.Database
|
||||
dumpFile := filepath.Join(os.TempDir(), fmt.Sprintf("db_dump_%s.sql", timestamp))
|
||||
|
||||
mysqldumpPath := s.findTool("mysqldump", config.AppConfig.Backup.MysqlDumpPath)
|
||||
|
||||
cmd := exec.Command(mysqldumpPath,
|
||||
"-h", dbCfg.Host,
|
||||
"-P", fmt.Sprintf("%d", dbCfg.Port),
|
||||
"-u", dbCfg.Username,
|
||||
"-p"+dbCfg.Password,
|
||||
"--single-transaction",
|
||||
"--routines",
|
||||
"--triggers",
|
||||
dbCfg.DBName,
|
||||
)
|
||||
|
||||
outFile, err := os.Create(dumpFile)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
cmd.Stdout = outFile
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
outFile.Close()
|
||||
os.Remove(dumpFile)
|
||||
return 0, fmt.Errorf("mysqldump执行失败: %w", err)
|
||||
}
|
||||
outFile.Close()
|
||||
|
||||
// 获取文件大小
|
||||
info, _ := os.Stat(dumpFile)
|
||||
dbSize := int64(0)
|
||||
if info != nil {
|
||||
dbSize = info.Size()
|
||||
}
|
||||
|
||||
// 写入tar
|
||||
file, err := os.Open(dumpFile)
|
||||
if err != nil {
|
||||
os.Remove(dumpFile)
|
||||
return 0, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
header := &tar.Header{
|
||||
Name: "database/" + dbCfg.DBName + ".sql",
|
||||
Size: dbSize,
|
||||
Mode: 0644,
|
||||
ModTime: time.Now(),
|
||||
}
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
os.Remove(dumpFile)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(tarWriter, file); err != nil {
|
||||
os.Remove(dumpFile)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
os.Remove(dumpFile)
|
||||
return dbSize, nil
|
||||
}
|
||||
|
||||
// backupFiles 备份文件
|
||||
func (s *BackupService) backupFiles(tarWriter *tar.Writer) (int64, int, error) {
|
||||
basePath := config.AppConfig.Storage.LocalBasePath
|
||||
totalSize := int64(0)
|
||||
count := 0
|
||||
|
||||
err := filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil // 跳过错误
|
||||
}
|
||||
// 跳过备份目录和临时目录
|
||||
if info.IsDir() && (info.Name() == ".backups" || info.Name() == ".tmp") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
relPath, _ := filepath.Rel(basePath, path)
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
header := &tar.Header{
|
||||
Name: "files/" + relPath,
|
||||
Size: info.Size(),
|
||||
Mode: int64(info.Mode()),
|
||||
ModTime: info.ModTime(),
|
||||
}
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := io.Copy(tarWriter, file); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
totalSize += info.Size()
|
||||
count++
|
||||
return nil
|
||||
})
|
||||
|
||||
return totalSize, count, err
|
||||
}
|
||||
|
||||
// finishLog 更新备份日志状态
|
||||
func (s *BackupService) finishLog(log *model.BackupLog, status, filePath string, fileSize int64, filesCount int, errMsg string) {
|
||||
now := time.Now()
|
||||
log.Status = status
|
||||
log.FilePath = filePath
|
||||
log.FileSize = fileSize
|
||||
log.FilesCount = filesCount
|
||||
log.FinishedAt = &now
|
||||
log.ErrorMsg = errMsg
|
||||
if !now.IsZero() && !log.StartedAt.IsZero() {
|
||||
log.Duration = int(now.Sub(log.StartedAt).Seconds())
|
||||
}
|
||||
config.DB.Save(log)
|
||||
}
|
||||
|
||||
// cleanOldBackups 清理旧备份
|
||||
func (s *BackupService) cleanOldBackups(policy *model.BackupPolicy) {
|
||||
var logs []model.BackupLog
|
||||
config.DB.Where("policy_id = ? AND status = 'success'", policy.ID).
|
||||
Order("started_at DESC").Offset(policy.MaxBackups).Find(&logs)
|
||||
|
||||
for _, log := range logs {
|
||||
// 删除备份文件
|
||||
if log.FilePath != "" {
|
||||
os.Remove(log.FilePath)
|
||||
}
|
||||
config.DB.Delete(&log)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 查询 ==========
|
||||
|
||||
// ListLogs 查询备份日志
|
||||
func (s *BackupService) ListLogs(policyID uint64, page, size int) ([]dto.BackupLogVO, int64, error) {
|
||||
query := config.DB.Model(&model.BackupLog{})
|
||||
if policyID > 0 {
|
||||
query = query.Where("policy_id = ?", policyID)
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var logs []model.BackupLog
|
||||
query.Order("started_at DESC").Offset((page - 1) * size).Limit(size).Find(&logs)
|
||||
|
||||
vos := make([]dto.BackupLogVO, 0, len(logs))
|
||||
for _, l := range logs {
|
||||
vo := dto.BackupLogVO{
|
||||
ID: l.ID,
|
||||
PolicyID: l.PolicyID,
|
||||
PolicyName: l.PolicyName,
|
||||
BackupType: l.BackupType,
|
||||
Status: l.Status,
|
||||
FilePath: l.FilePath,
|
||||
FileSize: l.FileSize,
|
||||
DBSize: l.DBSize,
|
||||
FilesCount: l.FilesCount,
|
||||
Duration: l.Duration,
|
||||
ErrorMsg: l.ErrorMsg,
|
||||
StartedAt: l.StartedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
if l.FinishedAt != nil {
|
||||
t := l.FinishedAt.Format("2006-01-02 15:04:05")
|
||||
vo.FinishedAt = &t
|
||||
}
|
||||
vos = append(vos, vo)
|
||||
}
|
||||
return vos, total, nil
|
||||
}
|
||||
|
||||
// ========== 恢复 ==========
|
||||
|
||||
// RestoreResult 恢复结果
|
||||
type RestoreResult struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
DBRestored bool `json:"db_restored"`
|
||||
FilesCount int `json:"files_count"`
|
||||
Duration int `json:"duration_ms"`
|
||||
}
|
||||
|
||||
// RestoreFromBackup 从备份文件恢复
|
||||
// logID: 备份日志ID
|
||||
// restoreDB: 是否恢复数据库
|
||||
// restoreFiles: 是否恢复文件
|
||||
func (s *BackupService) RestoreFromBackup(logID uint64, restoreDB, restoreFiles bool) (*RestoreResult, error) {
|
||||
// 获取备份日志
|
||||
var log model.BackupLog
|
||||
if err := config.DB.First(&log, logID).Error; err != nil {
|
||||
return nil, fmt.Errorf("备份记录不存在")
|
||||
}
|
||||
if log.Status != "success" {
|
||||
return nil, fmt.Errorf("只能恢复成功的备份")
|
||||
}
|
||||
if log.FilePath == "" {
|
||||
return nil, fmt.Errorf("备份文件路径为空")
|
||||
}
|
||||
|
||||
// 检查备份文件是否存在
|
||||
if _, err := os.Stat(log.FilePath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("备份文件不存在: %s", log.FilePath)
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
result := &RestoreResult{}
|
||||
|
||||
// 解压备份文件
|
||||
tempDir := filepath.Join(os.TempDir(), "restore_"+fmt.Sprintf("%d", logID))
|
||||
os.MkdirAll(tempDir, 0755)
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
if err := s.extractTarGz(log.FilePath, tempDir); err != nil {
|
||||
return nil, fmt.Errorf("解压备份文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 恢复数据库
|
||||
if restoreDB {
|
||||
dbFile := filepath.Join(tempDir, "database")
|
||||
if _, err := os.Stat(dbFile); err == nil {
|
||||
// 找到SQL文件
|
||||
entries, _ := os.ReadDir(dbFile)
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && filepath.Ext(entry.Name()) == ".sql" {
|
||||
sqlPath := filepath.Join(dbFile, entry.Name())
|
||||
if err := s.restoreDatabase(sqlPath); err != nil {
|
||||
result.Message = "数据库恢复失败: " + err.Error()
|
||||
result.Duration = int(time.Since(startTime).Milliseconds())
|
||||
return result, nil
|
||||
}
|
||||
result.DBRestored = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复文件
|
||||
if restoreFiles {
|
||||
filesDir := filepath.Join(tempDir, "files")
|
||||
if _, err := os.Stat(filesDir); err == nil {
|
||||
basePath := config.AppConfig.Storage.LocalBasePath
|
||||
count, err := s.restoreFiles(filesDir, basePath)
|
||||
if err != nil {
|
||||
result.Message = "文件恢复失败: " + err.Error()
|
||||
result.Duration = int(time.Since(startTime).Milliseconds())
|
||||
return result, nil
|
||||
}
|
||||
result.FilesCount = count
|
||||
}
|
||||
}
|
||||
|
||||
result.Success = true
|
||||
result.Duration = int(time.Since(startTime).Milliseconds())
|
||||
|
||||
parts := []string{}
|
||||
if result.DBRestored {
|
||||
parts = append(parts, "数据库已恢复")
|
||||
}
|
||||
if result.FilesCount > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%d个文件已恢复", result.FilesCount))
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
result.Message = "没有可恢复的内容"
|
||||
} else {
|
||||
result.Message = "恢复成功: " + strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// extractTarGz 解压tar.gz文件
|
||||
func (s *BackupService) extractTarGz(tarGzPath, destDir string) error {
|
||||
file, err := os.Open(tarGzPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
gzReader, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer gzReader.Close()
|
||||
|
||||
tarReader := tar.NewReader(gzReader)
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(destDir, header.Name)
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
os.MkdirAll(targetPath, 0755)
|
||||
case tar.TypeReg:
|
||||
os.MkdirAll(filepath.Dir(targetPath), 0755)
|
||||
outFile, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
io.Copy(outFile, tarReader)
|
||||
outFile.Close()
|
||||
os.Chmod(targetPath, os.FileMode(header.Mode))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// restoreDatabase 从SQL文件恢复数据库
|
||||
func (s *BackupService) restoreDatabase(sqlFilePath string) error {
|
||||
dbCfg := config.AppConfig.Database
|
||||
|
||||
mysqlPath := s.findTool("mysql", config.AppConfig.Backup.MysqlPath)
|
||||
|
||||
cmd := exec.Command(mysqlPath,
|
||||
"-h", dbCfg.Host,
|
||||
"-P", fmt.Sprintf("%d", dbCfg.Port),
|
||||
"-u", dbCfg.Username,
|
||||
"-p"+dbCfg.Password,
|
||||
dbCfg.DBName,
|
||||
)
|
||||
|
||||
sqlFile, err := os.Open(sqlFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sqlFile.Close()
|
||||
|
||||
cmd.Stdin = sqlFile
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// restoreFiles 恢复文件到目标目录
|
||||
func (s *BackupService) restoreFiles(srcDir, destDir string) (int, error) {
|
||||
count := 0
|
||||
err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
relPath, _ := filepath.Rel(srcDir, path)
|
||||
targetPath := filepath.Join(destDir, relPath)
|
||||
|
||||
if info.IsDir() {
|
||||
os.MkdirAll(targetPath, 0755)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 复制文件
|
||||
srcFile, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
os.MkdirAll(filepath.Dir(targetPath), 0755)
|
||||
dstFile, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
io.Copy(dstFile, srcFile)
|
||||
count++
|
||||
return nil
|
||||
})
|
||||
|
||||
return count, err
|
||||
}
|
||||
|
||||
// ListBackups 列出所有可恢复的备份文件
|
||||
func (s *BackupService) ListBackups() ([]dto.BackupLogVO, error) {
|
||||
var logs []model.BackupLog
|
||||
config.DB.Where("status = 'success' AND file_path != ''").
|
||||
Order("started_at DESC").Find(&logs)
|
||||
|
||||
vos := make([]dto.BackupLogVO, 0, len(logs))
|
||||
for _, l := range logs {
|
||||
vo := dto.BackupLogVO{
|
||||
ID: l.ID,
|
||||
PolicyName: l.PolicyName,
|
||||
BackupType: l.BackupType,
|
||||
FileSize: l.FileSize,
|
||||
FilesCount: l.FilesCount,
|
||||
StartedAt: l.StartedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
vos = append(vos, vo)
|
||||
}
|
||||
return vos, nil
|
||||
}
|
||||
|
||||
// findTool 查找可执行工具路径
|
||||
// 优先使用配置路径, 否则搜索常见路径
|
||||
func (s *BackupService) findTool(name, configPath string) string {
|
||||
if configPath != "" {
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
return configPath
|
||||
}
|
||||
}
|
||||
if _, err := exec.LookPath(name); err == nil {
|
||||
return name
|
||||
}
|
||||
commonPaths := []string{
|
||||
fmt.Sprintf(`C:\Program Files\MySQL\MySQL Server 8.0\bin\%s.exe`, name),
|
||||
fmt.Sprintf(`/usr/bin/%s`, name),
|
||||
}
|
||||
for _, p := range commonPaths {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
Reference in New Issue
Block a user