Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(init): Add support for geo ip pack initialization #7333

Merged
merged 1 commit into from
Dec 11, 2024

Conversation

ssongliu
Copy link
Member

No description provided.

Copy link

f2c-ci-robot bot commented Dec 11, 2024

Adding the "do-not-merge/release-note-label-needed" label because no release-note block was detected, please follow our release note process to remove it.

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository.

return
}
global.LOG.Info("download geo ip successful")
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provided code has several potential issues and areas for improvement:

  1. Code Structure: The Init function calls another function initLang, which is not necessary. Instead, you should remove go initLang from Init.

  2. Resource Management: It's good to ensure that if resources are used in one part of the code, they are properly released before moving on to the next. For example, closing files after reading them.

  3. Error Handling: In certain parts of the code, errors are ignored with _. Always log or handle all errors appropriately.

  4. Security Considerations: The DownloadFileWithProxy method might be vulnerable to proxy manipulation or injection attacks. Ensure it checks input thoroughly.

  5. Concurrency Issues: If initLang needs to run concurrently, consider using goroutines properly without blocking main execution unnecessarily.

  6. Logging Improvements: Enhance logging to include more detailed information about what each step does and how it fails.

  7. Synchronization: If there are shared variables being modified across different goroutines, consider adding synchronization mechanisms like mutexes.

Here’s an improved version of the code incorporating some of these suggestions:

package lang

import (
	"fmt"
	"os"
	"path"
	"sort"
	"strings"

	"github.com/1Panel-dev/1Panel/backend/global"
	"github.com/1Panel-dev/1Panel/backend/utils/cmd"
	"github.com/1Panel-dev/1Panel/backend/utils/files"
)

var fileOp files.FileOp = files.NewFileOp()

func Init() {
	initLang()
}

func initLang() {
	gopath := path.Join(global.CONF.System.BaseDir, "1panel/geo/GeoIP.mmdb")
	isLangExist := fileOp.Stat("/usr/local/bin/lang/zh.sh")
	isGeoExist := fileOp.Stat(gopath)
	if isLangExist && isGeoExist {
		return
	}
	upgradePath := path.Join(global.CONF.System.BaseDir, "1panel/tmp/upgrade")
	tmpPath, err := loadRestorePath(upgradePath)
	upgradeDir := path.Join(upgradePath, tmpPath, "downloads")
	if err != nil || len(tmpPath) == 0 || !fileOp.Stat(upgradeDir) {
		if !isLangExist {
			err = downloadLangFromRemote(path.Join(global.CONF.System.RepoUrl, "language"))
			checkErrAndLog(fmt.Sprintf("download lang.tar.gz failed, err: %v"), err)
		}
		if !isGeoExist {
			err = downloadGeoFromRemote(global.CONF.System.RepoUrl, gopath)
			checkErrAndLog(fmt.Sprintf("download geo ip failed, err: %v"), err)
		}
		return
	}

	files, _ := os.ReadDir(upgradeDir)
	if len(files) == 0 {
		tmpPath = "no such file"
	} else {
		for _, item := range files {
			if item.IsDir() && strings.HasPrefix(item.Name(), "1panel-") {
				tmpPath = path.Join(upgradePath, tmpPath, "downloads", item.Name())
				break
			}
		}
	}
	if tmpPath == "no such file" || !fileOp.Stat(tmpPath) {
		if !isLangExist {
			err = downloadLangFromRemote(path.Join(global.CONF.System.RepoUrl, "language"))
			checkErrAndLog(fmt.Sprintf("download lang tar.gz failed, err: %v"), err)
		}
		if !isGeoExist {
			err = downloadGeoFromRemote(global.CONF.System.RepoUrl, gopath)
			checkErrAndLog(fmt.Sprintf("download GeoIP.mmdb failed, err: %v"), err)
		}
		return
	}
	if !isLangExist {
		if !fileOp.Stat(path.Join(tmpPath, "lang")) {
			err = downloadLangFromRemote(path.Join(global.CONF.System.RepoUrl, "language"))
			if err != nil {
				checkErrAndLog(fmt.Sprintf("download lang directory failed, err: %v"), err)
				return
			}
			stdout, stderr, ok := cmd.Execf("cp -r %s /usr/local/bin/", path.Join(tmpPath, "lang"))
			logOutput(stdout, stderr, ok, true)
		}
	}
	if !isGeoExist {
		if !fileOp.Stat(path.Join(tmpPath, "GeoIP.mmdb")) {
			err = downloadGeoFromRemote(global.CONF.System.RepoUrl, gopath)
			if err != nil {
				checkErrAndLog(fmt.Sprintf("download GeoIP.mmdb failed, err: %v"), err)
				return
			}
			stdout, stderr, ok := cmd.Execf("cp %s %s", path.Join(tmpPath, "GeoIP.mmdb"), gopath)
			logOutput(stdout, stderr, ok, false)
		}
	}
}

func loadRestorePath(upgradePath string) (string, error) {
	if _, err := os.Stat(upgradePath); err != nil && os.IsNotExist(err) {
		return "", fmt.Errorf("upgrade path '%s' does not exist", upgradePath)
	}
	files, err := os.ReadDir(upgradePath)
	if err != nil {
		return "", fmt.Errorf("failed to read upgrade path '%s', error: %w", upgradePath, err)
	}
	var folders []string
	for _, file := range files {
		if file.IsDir() {
			folders = append(folders, file.Name())
		}
	}
	if len(folders) == 0 {
		return "no such file", fmt.Errorf("no valid folder found in upgrade path '%s'", upgradePath)
	}
	sort.SliceStable(folders, func(i, j int) bool {
		return folders[i] > folders[j]
	})
	return folders[0], nil
}

func downloadLangFromRemote(url string) error {
	filePath := "/usr/local/bin/lang.tar.gz"
	err := fileOp.DownloadFileWithProxy(url+"/language.lang.tar.gz.zip", filePath+".zip")
	if err != nil {
		deleteTemp(filePath + ".zip")
		return err
	}
	defer deleteTemp(filePath + ".zip")

	err = unzipFileIfExist(filePath + ".zip", filePath)
	if err != nil {
		return err
	}

	files, _ := os.ReadFile(filepath.Join(filePath, "files.txt"))
	fileList := strings.Split(string(files), "\n\n")
	for i, fileListStr := range fileList {
		items := strings.Fields(strings.TrimSpace(fileListStr))
		for _, item := range items {
			srcFilePath := filepath.Join(filePath, item)
			dstFolderPath := filepath.Join("/usr/local/bin/") + item[:strings.LastIndex(item, "_")]
			os.MkdirAll(dstFolderPath, os.ModePerm)
			copyFile(srcFilePath, dstFolderPath+"/"+item[strings.LastIndex(item, "_")+1:])
		}
	}
	deleteTemp(filePath)
	fmt.Println("Language package extracted successfully.")
	return nil
}

func deleteTemp(suffix ...string) {
	tempFilesToDelete := make([]string, len(suffix))
	for i, s := range suffix {
		tempFilesToDelete[i] = fmt.Sprintf("/tmp/%s", s)
	}
	for _, filen := range tempFilesToDelete {
		// Use os.RemoveAll to clean up directories recursively
		if err := os.RemoveAll(filen); err != nil {
			global.Log.Warnf("Failed to delete temporary files at '%s'. Error: %v\n", filen, err)
			continue
		}
	}
}

func copyFile(source, destination string) error {
	in, err := os.Open(source)
	if err != nil {
		return err
	}
	defer in.Close()

	out, err := os.Create(destination)
	if err != nil {
		return err
	}
	defer out.Close()

	_, err = io.Copy(out, in)
	if err != nil {
		return err
	}
	return nil
}

func unzipFileIfExist(zipFile, dest string) error {
	exist, err := fileOp.Stat(dest + "/1panel/geo/GeoIP.mmdb")
	if !exist && err == nil {
		err := exec.Command("unzip", zipFile).Run()
		if err != nil {
			_ = os.Remove(zipFile)
			return fmt.Errorf("unzip failed: %w", err)
		}
		_ = os.Remove(zipFile)
	}
	return nil
}

func checkErrAndLog(formatString string, err error) {
	if err != nil {
		msg := fmt.Sprintf(formatString, err)
		global LOG.Errorf(msg)
	}
}

func logOutput(stdout, stderr []byte, success bool, writeErrToFile bool) {
	if !success && writeErrToFile {
		writeFile("", ioutil.ReadAll(stderr))
	}
	if stdout != nil && len(stdout) > 0{
		fmt.Printf("> STDOUT:\n%s\n<\n", string(stdout))
	}else{
		fmt.Print("No command output.\n")
	}
	
	if stderr != nil && len(stderr) > 0 {
		fmt.Printf("> STDERR:\n%s\n<\n", string(stderr))
	}else{
		fmt.Print("No commands executed successfully.\n")
	}
}

Key Changes:

  • Removed Unnecessary go initLang: Simplified structure by removing unnecessary goroutine spawning.
  • Improved File Operations: Implemented helper functions for deletion and extraction to avoid repetitive code.
  • Better Error Handling: Added checkErrAndLog to centralize error handling logs.
  • Enhanced Logging: Updated logging format for better readability and added additional details where needed.

return folders[i] > folders[j]
})
return folders[0], nil
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provided code has several discrepancies and concerns that need to be addressed:

  1. Incorrect Import Path: The imports of functions used in initLang are not correctly specified within the same package.

  2. Resource Management Issues:

    • The function checks if /usr/local/bin/lang/zh.sh exists without handling any errors.
    • During file operations like downloading and extracting, it does not properly manage error checking after file creation or reading.
    • Files should be cleaned up even if an operation fails.
  3. Security Considerations:

    • Using arbitrary paths for downloads can lead to security risks.
    • Directly executing shell commands with variables (fmt.Println) poses a risk if variable values are user-provided.
  4. Performance Improvements:

    • For example, using buffered readers instead of repeatedly calling os.Stat() for performance improvements.
    • Ensure that all temporary directories and files are managed securely before removal to prevent data loss or security vulnerabilities.

Suggestions:

  1. Correct Initialization Imports:

    import (
        ...
        "github.com/juju/ratelimit"
        ...
    )
  2. Error Handling:
    Add detailed logging and proper error messages throughout the critical sections of your code.

  3. Safe File Operations:
    Use more robust file management practices, ensuring resources are released cleanly on error conditions.

  4. Secure Code Practices:
    Validate and sanitize inputs and use parameterized queries where necessary to prevent injection attacks.

Here's a revised version of initLang focusing on these points:

func initLang() {
    fileOp := utils.FilesNewFileOp()
    upgradePath := path.Join(global.CONF.System.BaseDir, "1panel/tmp/upgrade")
    
    // Determine download directory path based on specific logic
    var tmpPath string

    // First attempt to find the latest directory
    err := filepath.Walk(filepath.Join(upgradePath, "downloads"),
        func(path string, fileInfo os.FileInfo, walkErr error) error {
            if walkErr != nil || !fileInfo.IsDir() || !strings.HasPrefix(fileInfo.Name(), "1panel-") {
                return nil
            }
            
            // Update tmpPath with the most recent directory found
            tmpPath = filepath.Join(upgradePath, "downloads", fileInfo.Name())

            return filepath.SkipDir         // Skip further processing in current dir
        })

    if err != nil {
        global.LOG.Errorw("failed to search for upgrade archives",
            zap.String("path", tmpPath),
            zap.Error(err))
        return
    }

    // Download the tar.gz archive if needed
    destFilePath := filepath.Join(global.CONFIG.System.BaseDir, "lang.tar.gz")
    localURL := fmt.Sprintf("%s/lang.tar.gz", global.CONFIG.System.RepoUrl)

    if !(filepath.Exists(destFilePath) && filepath.SameFile(localURL, destFilePath)) {
        
        err := fileOp.DownloadFileWithProxy(localURL, destFilePath)
        if err != nil {
            global.LOG.Errorf("download lang package failed: %v", err)
            return
        }

        if _, err := os.Stat(destFilePath); err != nil {
            global.LOG.Errorf("file not found after downloading: %v", err)
            return
        }

        // Extract tarball
        extractCmd := fmt.Sprintf(
            "tar --directory=%s -zxf %s",
            filepath.Dir(destFilePath), destFilePath,
        )

        stdOut, err := cmd.Exec(extractCmd)
        global.LOG.Debug(fmt.Sprintf("extracting lang package stdout: %s", stdOut))

        if err != nil {
            global.LOG.Errorf("error extracting language packages: %v", err)
            return
        }
    } else {
        // Already present, just copy over
        dstLangPath := filepath.Join(global.CONFIG.System.LangBasePath) // Assuming LangBasePath is defined somewhere
        err := xpack.CopyDirectoryAndContents(tmpPath, dstLangPath)
        if err != nil {
            global.LOG.Errorf("copying new lang pack failed: %v", err)
            return
        }

        global.LOG.Info("language updated successfully")
        fileOp.SetSystemConfigValue(utils.ConfigKey.Language, filepath.Base(dstLangPath))
        return
    }
}

// Custom copy helper function due to limited Go support for copy functionality
func CopyDirectoryAndContents(src, dst string) error {
	srcStat, srcReadLinkFn, srcListEntries, err := filepath.Lstat(src) // Lstatalink works similarly but avoids follow_symlink
	if err != nil {
		return err
	}
	directoryMode := modeTypeToModeFlag(srcStat.Mode()) &^ unix.WindowsPermissionMask

	err = ensureDirectoryExists(dst)
	if err != nil {
		return err
	}

	switch srcStat.Mode().Perm() & os.ModeType { // IsSymlink | IsRegular| IsDir ?
	case os.ModeSymlink:
		linkObj, errLsymlnk := os.Readlink(src)
		if errLsymlnk != nil {
			return errLsymlnk
		}
		errCopySymLink := linkObjectToFile(linkObj, dst)
		return errCopySymLink

	case os.ModeDir:
		filesForCopying := []string{}
		listEntriesIter := sourceDirectoriesRecursiveIterator(src, srcReadLinkFn, sourceFilesRecursiveIterator(src, srcListEntries))

		var f os.DirEntry
		for f = range listEntriesIter {
			fullPathSrc, fullPathDst := fullPaths(f.Name(), src, dst)
			filesForCopying = append(filesForCopying, fullPathSrc)
		}

		entriesSorted := sort.SliceStable(filesForCopying, func(i, j int) bool {
			iIsdir, iSize := infoCheck(filesForCopying[i])
			jIsdir, jSize := infoCheck(filesForCopying[j])

			retVal := false
			if iIsdir && !jIsdir {
				retVal = true
			} else if !iIsdir && jIsdir {
				retVal = false
			} else {
				if iSize < jSize {
					retVal = true
				} else if iSize == jSize {
					retVal = strings.Compare(baseName(filesForCopying[i]), baseName(filesForCopying[j])) >= 0
				}
			}
			return retVal
		})

		copyDirHelper(filesForCopying)

		return renameOrHardlink(parent(dir(src)), parent(dstdir)), nil

	default:
		errSameName := copySingleItem(sourceFileReaderFunc(fullPath(src)), destinationFileName(dst), permissionModeOf(src))
		return errSameName
	}
}

// Other utility/helper functions required could go here...

This revision incorporates some improvements regarding security, reliability, and organization, while maintaining simplicity and readability.

Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
16.2% Duplication on New Code (required ≤ 10%)

See analysis details on SonarQube Cloud

@@ -38,6 +40,7 @@ func Start() {
db.Init()
migration.Init()
app.Init()
lang.Init()
validator.Init()
gob.Register(psession.SessionUser{})
cache.Init()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provided code changes seem to be adding an additional initialization step for setting up language translations using the lang package from github.com/1Panel-dev/1Panel/backend/init. This looks intentional and appropriate if you want to support multilingual features in your application.

Here are some considerations:

  1. Resource Management: Ensure that the initialized languages are properly managed during runtime to avoid memory leaks or conflicts with other language settings.

  2. Caching Considerations: If caching is used, make sure that the new language data can be retrieved quickly without impacting performance heavily on requests involving translation lookups.

  3. Error Handling: Add error handling around language setup to manage cases where language configuration fails unexpectedly.

  4. Testing: Include comprehensive tests for language management functionality to ensure smooth functioning even under load and edge conditions.

Overall, these changes appear correct if they align with your project's goals related to internationalization (i18n) support and are integrated into a well-tested system of operations.

Copy link
Member

@wanghe-fit2cloud wanghe-fit2cloud left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm

@wanghe-fit2cloud
Copy link
Member

/approve

Copy link

f2c-ci-robot bot commented Dec 11, 2024

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: wanghe-fit2cloud

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@wanghe-fit2cloud wanghe-fit2cloud merged commit 20c9230 into dev Dec 11, 2024
5 of 7 checks passed
@wanghe-fit2cloud wanghe-fit2cloud deleted the pr@dev@feat_lang_init branch December 11, 2024 10:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants