mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
feat: support browser core archive imports
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"ant-chrome/backend/internal/config"
|
||||
"ant-chrome/backend/internal/logger"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -86,7 +87,7 @@ func (a *App) BrowserCoreScan() []BrowserCore {
|
||||
return a.browserMgr.ListCores()
|
||||
}
|
||||
|
||||
// BrowserCoreImportLocal 选择一个已解压内核目录并直接注册,不下载、不复制文件。
|
||||
// BrowserCoreImportLocal 选择一个已解压内核目录或归档文件并注册。
|
||||
func (a *App) BrowserCoreImportLocal() (*BrowserCore, error) {
|
||||
if a.ctx == nil {
|
||||
return nil, fmt.Errorf("app context is nil")
|
||||
@@ -95,6 +96,104 @@ func (a *App) BrowserCoreImportLocal() (*BrowserCore, error) {
|
||||
return nil, fmt.Errorf("browser manager is nil")
|
||||
}
|
||||
|
||||
selectedPath, err := wailsruntime.OpenFileDialog(a.ctx, wailsruntime.OpenDialogOptions{
|
||||
Title: "选择 Chrome 内核归档文件",
|
||||
Filters: []wailsruntime.FileFilter{
|
||||
{DisplayName: "Chrome 内核归档 (" + browser.SupportedCoreArchiveDescription() + ")", Pattern: browser.SupportedCoreArchivePattern()},
|
||||
{DisplayName: "所有文件 (*.*)", Pattern: "*.*"},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selectedPath = strings.TrimSpace(selectedPath)
|
||||
if selectedPath == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(selectedPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.importLocalBrowserCoreArchive(absPath)
|
||||
}
|
||||
|
||||
func (a *App) importLocalBrowserCoreArchive(archivePath string) (*BrowserCore, error) {
|
||||
archiveName := strings.TrimSpace(filepath.Base(archivePath))
|
||||
coreName := strings.TrimSpace(coreNameFromArchiveName(archiveName))
|
||||
if coreName == "" {
|
||||
coreName = "本地内核"
|
||||
}
|
||||
|
||||
targetCorePath := filepath.Join("chrome", coreName)
|
||||
targetDir := a.browserMgr.ResolveRelativePath(targetCorePath)
|
||||
if _, err := os.Stat(targetDir); err == nil {
|
||||
return nil, fmt.Errorf("同名内核目录已存在:%s", targetCorePath)
|
||||
} else if err != nil && !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parentDir := filepath.Dir(targetDir)
|
||||
if err := os.MkdirAll(parentDir, 0o755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tempExtractDir, err := os.MkdirTemp(parentDir, coreName+"_import_*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cleanupTempExtract := true
|
||||
defer func() {
|
||||
if cleanupTempExtract {
|
||||
_ = os.RemoveAll(tempExtractDir)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := browser.ExtractCoreArchiveAndStripRootForImport(archivePath, tempExtractDir); err != nil {
|
||||
return nil, fmt.Errorf("解压失败: %w", err)
|
||||
}
|
||||
if _, _, ok := browser.FindCoreExecutable(tempExtractDir); !ok {
|
||||
return nil, fmt.Errorf("所选归档不是当前平台可用的内核包:当前平台 %s,未找到浏览器可执行文件(候选:%s)", browser.CoreExecutablePlatform(), strings.Join(browser.CoreExecutableCandidates(), ", "))
|
||||
}
|
||||
if err := os.Rename(tempExtractDir, targetDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cleanupTempExtract = false
|
||||
|
||||
input := browser.CoreInput{
|
||||
CoreName: coreName,
|
||||
CorePath: targetCorePath,
|
||||
IsDefault: len(a.browserMgr.ListCores()) == 0,
|
||||
}
|
||||
if err := a.browserMgr.SaveCore(input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, saved := range a.browserMgr.ListCores() {
|
||||
if normalizeCorePathForCompare(saved.CorePath) == normalizeCorePathForCompare(targetCorePath) {
|
||||
return &saved, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("本地内核已保存但未能读取结果")
|
||||
}
|
||||
|
||||
func coreNameFromArchiveName(name string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
for _, suffix := range []string{".tar.gz", ".tar.xz", ".tar.bz2", ".tgz", ".txz", ".tbz2", ".zip", ".tar"} {
|
||||
if strings.HasSuffix(strings.ToLower(name), suffix) {
|
||||
return strings.TrimSpace(name[:len(name)-len(suffix)])
|
||||
}
|
||||
}
|
||||
return strings.TrimSuffix(name, filepath.Ext(name))
|
||||
}
|
||||
|
||||
// BrowserCoreImportLocalDirectory 选择一个已解压内核目录并直接注册,不下载、不复制文件。
|
||||
func (a *App) BrowserCoreImportLocalDirectory() (*BrowserCore, error) {
|
||||
if a.ctx == nil {
|
||||
return nil, fmt.Errorf("app context is nil")
|
||||
}
|
||||
if a.browserMgr == nil {
|
||||
return nil, fmt.Errorf("browser manager is nil")
|
||||
}
|
||||
|
||||
selectedDir, err := wailsruntime.OpenDirectoryDialog(a.ctx, wailsruntime.OpenDialogOptions{
|
||||
Title: "选择已解压的 Chrome 内核目录",
|
||||
})
|
||||
@@ -111,7 +210,7 @@ func (a *App) BrowserCoreImportLocal() (*BrowserCore, error) {
|
||||
return nil, err
|
||||
}
|
||||
if _, _, ok := browser.FindCoreExecutable(absDir); !ok {
|
||||
return nil, fmt.Errorf("所选目录不是有效内核目录:未找到浏览器可执行文件(候选:%s)", strings.Join(browser.CoreExecutableCandidates(), ", "))
|
||||
return nil, fmt.Errorf("所选目录不是当前平台可用的内核目录:当前平台 %s,未找到浏览器可执行文件(候选:%s)", browser.CoreExecutablePlatform(), strings.Join(browser.CoreExecutableCandidates(), ", "))
|
||||
}
|
||||
|
||||
corePath := a.relativeCorePathIfPossible(absDir)
|
||||
|
||||
@@ -186,7 +186,7 @@ func (a *App) scanChromeDir(chromeRoot string) []browser.Core {
|
||||
}
|
||||
|
||||
// 如果根目录本身就有浏览器可执行文件,视为单内核结构
|
||||
if _, _, ok := browser.FindCoreExecutable(baseDir); ok {
|
||||
if _, _, ok := browser.FindCoreExecutableShallow(baseDir); ok {
|
||||
return []browser.Core{
|
||||
{
|
||||
CoreId: "default",
|
||||
|
||||
@@ -13,7 +13,7 @@ func CoreExecutableCandidates() []string {
|
||||
case "windows":
|
||||
return []string{"chrome.exe"}
|
||||
case "linux":
|
||||
return []string{"chrome", "chrome-bin", "chrome.exe"}
|
||||
return []string{"chrome", "chrome-bin", "chromium", "chromium-browser", "ungoogled-chromium", "chrome.exe"}
|
||||
case "darwin":
|
||||
return []string{
|
||||
"Google Chrome.app/Contents/MacOS/Google Chrome",
|
||||
@@ -25,8 +25,22 @@ func CoreExecutableCandidates() []string {
|
||||
}
|
||||
}
|
||||
|
||||
func CoreExecutablePlatform() string {
|
||||
return goruntime.GOOS + "/" + goruntime.GOARCH
|
||||
}
|
||||
|
||||
// FindCoreExecutable 在指定目录查找可执行文件,返回绝对路径和命中的候选名。
|
||||
func FindCoreExecutable(baseDir string) (string, string, bool) {
|
||||
if directPath, directCandidate, ok := FindCoreExecutableShallow(baseDir); ok {
|
||||
return directPath, directCandidate, true
|
||||
}
|
||||
if recursivePath, recursiveCandidate, ok := findNestedCoreExecutable(baseDir); ok {
|
||||
return recursivePath, recursiveCandidate, true
|
||||
}
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
func FindCoreExecutableShallow(baseDir string) (string, string, bool) {
|
||||
baseDir = strings.TrimSpace(baseDir)
|
||||
if baseDir == "" {
|
||||
return "", "", false
|
||||
@@ -46,6 +60,44 @@ func FindCoreExecutable(baseDir string) (string, string, bool) {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
func findNestedCoreExecutable(baseDir string) (string, string, bool) {
|
||||
info, err := os.Stat(baseDir)
|
||||
if err != nil || !info.IsDir() {
|
||||
return "", "", false
|
||||
}
|
||||
baseDepth := strings.Count(filepath.ToSlash(filepath.Clean(baseDir)), "/")
|
||||
candidateNames := make(map[string]string)
|
||||
for _, candidate := range CoreExecutableCandidates() {
|
||||
candidateNames[strings.ToLower(filepath.Base(candidate))] = candidate
|
||||
}
|
||||
|
||||
var matchedPath string
|
||||
var matchedCandidate string
|
||||
_ = filepath.WalkDir(baseDir, func(path string, entry os.DirEntry, err error) error {
|
||||
if err != nil || path == baseDir || matchedPath != "" {
|
||||
return nil
|
||||
}
|
||||
if entry.IsDir() {
|
||||
depth := strings.Count(filepath.ToSlash(filepath.Clean(path)), "/") - baseDepth
|
||||
if depth > 5 {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
candidate, ok := candidateNames[strings.ToLower(entry.Name())]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
matchedPath = path
|
||||
matchedCandidate = candidate
|
||||
return nil
|
||||
})
|
||||
if matchedPath == "" {
|
||||
return "", "", false
|
||||
}
|
||||
return matchedPath, matchedCandidate, true
|
||||
}
|
||||
|
||||
func findDirectCoreExecutable(path string) (string, string, bool) {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || info.IsDir() {
|
||||
|
||||
@@ -1,107 +1,311 @@
|
||||
package browser
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"compress/bzip2"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/ulikunitz/xz"
|
||||
)
|
||||
|
||||
// extractZipAndStripRoot 解压 ZIP 包,如果其所有文件全被同一个根目录包裹,则剥离这层根目录解压至 dest
|
||||
// progressCb 为进度回调 (0-100%, statusType_msg)
|
||||
func extractZipAndStripRoot(zipPath, dest string, progressCb func(int, string)) error {
|
||||
r, err := zip.OpenReader(zipPath)
|
||||
type archiveEntry struct {
|
||||
Name string
|
||||
Mode os.FileMode
|
||||
Dir bool
|
||||
Open func() (io.ReadCloser, error)
|
||||
LinkName string
|
||||
Symlink bool
|
||||
}
|
||||
|
||||
func SupportedCoreArchivePattern() string {
|
||||
return "*.zip;*.tar;*.tar.gz;*.tgz;*.tar.xz;*.txz;*.tar.bz2;*.tbz2"
|
||||
}
|
||||
|
||||
func SupportedCoreArchiveDescription() string {
|
||||
return "支持 ZIP、TAR、TAR.GZ、TAR.XZ、TAR.BZ2"
|
||||
}
|
||||
|
||||
func coreArchiveTempPattern(rawURL string) string {
|
||||
lowerName := strings.ToLower(strings.TrimSpace(rawURL))
|
||||
if parsed, err := filepathFromURLPath(lowerName); err == nil && parsed != "" {
|
||||
lowerName = parsed
|
||||
}
|
||||
suffixes := []string{".tar.gz", ".tar.xz", ".tar.bz2", ".tgz", ".txz", ".tbz2", ".zip", ".tar"}
|
||||
for _, suffix := range suffixes {
|
||||
if strings.HasSuffix(lowerName, suffix) {
|
||||
return "download_*" + suffix
|
||||
}
|
||||
}
|
||||
return "download_*"
|
||||
}
|
||||
|
||||
func filepathFromURLPath(raw string) (string, error) {
|
||||
parts := strings.SplitN(raw, "?", 2)
|
||||
parts = strings.SplitN(parts[0], "#", 2)
|
||||
return filepath.Base(parts[0]), nil
|
||||
}
|
||||
|
||||
func extractCoreArchiveAndStripRoot(archivePath, dest string, progressCb func(int, string)) error {
|
||||
entries, closeEntries, err := openCoreArchiveEntries(archivePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
defer closeEntries()
|
||||
|
||||
if len(r.File) == 0 {
|
||||
if len(entries) == 0 {
|
||||
return fmt.Errorf("空的压缩包")
|
||||
}
|
||||
|
||||
// 探测是否存在单一顶层目录
|
||||
var rootPrefix string
|
||||
hasCommonRoot := true
|
||||
|
||||
for _, f := range r.File {
|
||||
cleanName := filepath.ToSlash(f.Name)
|
||||
parts := strings.SplitN(cleanName, "/", 2)
|
||||
|
||||
// 检查空名称文件,理论上不该有
|
||||
if len(parts) == 0 || parts[0] == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if rootPrefix == "" {
|
||||
rootPrefix = parts[0] + "/"
|
||||
} else if !strings.HasPrefix(cleanName, rootPrefix) && cleanName != strings.TrimSuffix(rootPrefix, "/") {
|
||||
hasCommonRoot = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(dest, 0755); err != nil {
|
||||
rootPrefix, hasCommonRoot := detectCommonArchiveRoot(entries)
|
||||
if err := os.MkdirAll(dest, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
totalFiles := len(r.File)
|
||||
for i, f := range r.File {
|
||||
// 报告进度 (逢 5% 更新一下)
|
||||
percent := int((float64(i) / float64(totalFiles)) * 100)
|
||||
if i%50 == 0 {
|
||||
progressCb(percent, fmt.Sprintf("正在解压文件 %d / %d...", i+1, totalFiles))
|
||||
for index, entry := range entries {
|
||||
percent := int((float64(index) / float64(len(entries))) * 100)
|
||||
if index%50 == 0 {
|
||||
progressCb(percent, fmt.Sprintf("正在解压文件 %d / %d...", index+1, len(entries)))
|
||||
}
|
||||
|
||||
cleanName := filepath.ToSlash(f.Name)
|
||||
cleanName := normalizeArchiveEntryName(entry.Name)
|
||||
if hasCommonRoot {
|
||||
if cleanName == rootPrefix || cleanName == strings.TrimSuffix(rootPrefix, "/") {
|
||||
// 忽略外包装本层目录条目
|
||||
continue
|
||||
}
|
||||
cleanName = strings.TrimPrefix(cleanName, rootPrefix)
|
||||
}
|
||||
|
||||
if cleanName == "" || cleanName == "/" {
|
||||
continue
|
||||
}
|
||||
|
||||
fpath := filepath.Join(dest, filepath.FromSlash(cleanName))
|
||||
// 防止 Zip Slip 漏洞
|
||||
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
|
||||
return fmt.Errorf("非法文件路径: %s", fpath)
|
||||
}
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
os.MkdirAll(fpath, f.Mode())
|
||||
continue
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(fpath), 0755); err != nil {
|
||||
targetPath, err := safeArchiveTargetPath(dest, cleanName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开解压文件写入失败 %s: %v", fpath, err)
|
||||
if entry.Dir {
|
||||
if err := os.MkdirAll(targetPath, entry.Mode.Perm()); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if entry.Symlink {
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.Remove(targetPath)
|
||||
if err := os.Symlink(entry.LinkName, targetPath); err != nil {
|
||||
return fmt.Errorf("创建符号链接失败 %s: %w", cleanName, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
outFile.Close()
|
||||
return fmt.Errorf("读取压缩包文件失败 %s: %v", f.Name, err)
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(outFile, rc)
|
||||
outFile.Close()
|
||||
rc.Close()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("写入文件流失败 %s: %v", fpath, err)
|
||||
if err := writeArchiveEntryFile(targetPath, entry); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
progressCb(100, "解压完成!")
|
||||
return nil
|
||||
}
|
||||
|
||||
func ExtractCoreArchiveAndStripRootForImport(archivePath, dest string) error {
|
||||
return extractCoreArchiveAndStripRoot(archivePath, dest, func(int, string) {})
|
||||
}
|
||||
|
||||
func openCoreArchiveEntries(archivePath string) ([]archiveEntry, func(), error) {
|
||||
lower := strings.ToLower(archivePath)
|
||||
if strings.HasSuffix(lower, ".zip") {
|
||||
return openZipArchiveEntries(archivePath)
|
||||
}
|
||||
if isTarArchivePath(lower) {
|
||||
return openTarArchiveEntries(archivePath)
|
||||
}
|
||||
if entries, closeEntries, err := openZipArchiveEntries(archivePath); err == nil {
|
||||
return entries, closeEntries, nil
|
||||
}
|
||||
return openTarArchiveEntries(archivePath)
|
||||
}
|
||||
|
||||
func openZipArchiveEntries(archivePath string) ([]archiveEntry, func(), error) {
|
||||
reader, err := zip.OpenReader(archivePath)
|
||||
if err != nil {
|
||||
return nil, func() {}, err
|
||||
}
|
||||
entries := make([]archiveEntry, 0, len(reader.File))
|
||||
for _, file := range reader.File {
|
||||
zipFile := file
|
||||
entries = append(entries, archiveEntry{
|
||||
Name: zipFile.Name,
|
||||
Mode: zipFile.Mode(),
|
||||
Dir: zipFile.FileInfo().IsDir(),
|
||||
Open: func() (io.ReadCloser, error) {
|
||||
return zipFile.Open()
|
||||
},
|
||||
})
|
||||
}
|
||||
return entries, func() { _ = reader.Close() }, nil
|
||||
}
|
||||
|
||||
func openTarArchiveEntries(archivePath string) ([]archiveEntry, func(), error) {
|
||||
file, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
return nil, func() {}, err
|
||||
}
|
||||
reader, err := tarStreamReader(archivePath, file)
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return nil, func() {}, err
|
||||
}
|
||||
|
||||
tmpDir, err := os.MkdirTemp(filepath.Dir(archivePath), "archive_entries_*")
|
||||
if err != nil {
|
||||
_ = file.Close()
|
||||
return nil, func() {}, err
|
||||
}
|
||||
cleanup := func() {
|
||||
_ = file.Close()
|
||||
_ = os.RemoveAll(tmpDir)
|
||||
}
|
||||
|
||||
tarReader := tar.NewReader(reader)
|
||||
var entries []archiveEntry
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, func() {}, err
|
||||
}
|
||||
|
||||
mode := header.FileInfo().Mode()
|
||||
entry := archiveEntry{
|
||||
Name: header.Name,
|
||||
Mode: mode,
|
||||
Dir: header.FileInfo().IsDir(),
|
||||
LinkName: header.Linkname,
|
||||
Symlink: header.Typeflag == tar.TypeSymlink,
|
||||
}
|
||||
if header.Typeflag == tar.TypeReg || header.Typeflag == tar.TypeRegA {
|
||||
spoolPath := filepath.Join(tmpDir, fmt.Sprintf("entry_%06d", len(entries)))
|
||||
out, err := os.OpenFile(spoolPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode.Perm())
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, func() {}, err
|
||||
}
|
||||
_, copyErr := io.Copy(out, tarReader)
|
||||
closeErr := out.Close()
|
||||
if copyErr != nil {
|
||||
cleanup()
|
||||
return nil, func() {}, copyErr
|
||||
}
|
||||
if closeErr != nil {
|
||||
cleanup()
|
||||
return nil, func() {}, closeErr
|
||||
}
|
||||
entry.Open = func() (io.ReadCloser, error) {
|
||||
return os.Open(spoolPath)
|
||||
}
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
return entries, cleanup, nil
|
||||
}
|
||||
|
||||
func tarStreamReader(archivePath string, file *os.File) (io.Reader, error) {
|
||||
lower := strings.ToLower(archivePath)
|
||||
switch {
|
||||
case strings.HasSuffix(lower, ".tar.gz") || strings.HasSuffix(lower, ".tgz"):
|
||||
return gzip.NewReader(file)
|
||||
case strings.HasSuffix(lower, ".tar.xz") || strings.HasSuffix(lower, ".txz"):
|
||||
return xz.NewReader(file)
|
||||
case strings.HasSuffix(lower, ".tar.bz2") || strings.HasSuffix(lower, ".tbz2"):
|
||||
return bzip2.NewReader(file), nil
|
||||
case strings.HasSuffix(lower, ".tar"):
|
||||
return file, nil
|
||||
default:
|
||||
return file, nil
|
||||
}
|
||||
}
|
||||
|
||||
func isTarArchivePath(path string) bool {
|
||||
for _, suffix := range []string{".tar", ".tar.gz", ".tgz", ".tar.xz", ".txz", ".tar.bz2", ".tbz2"} {
|
||||
if strings.HasSuffix(path, suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func detectCommonArchiveRoot(entries []archiveEntry) (string, bool) {
|
||||
var rootPrefix string
|
||||
for _, entry := range entries {
|
||||
cleanName := normalizeArchiveEntryName(entry.Name)
|
||||
parts := strings.SplitN(cleanName, "/", 2)
|
||||
if len(parts) == 0 || parts[0] == "" {
|
||||
continue
|
||||
}
|
||||
if rootPrefix == "" {
|
||||
rootPrefix = parts[0] + "/"
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(cleanName, rootPrefix) && cleanName != strings.TrimSuffix(rootPrefix, "/") {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
return rootPrefix, rootPrefix != ""
|
||||
}
|
||||
|
||||
func normalizeArchiveEntryName(name string) string {
|
||||
cleanName := filepath.ToSlash(strings.TrimSpace(name))
|
||||
cleanName = strings.TrimPrefix(cleanName, "/")
|
||||
return filepath.ToSlash(filepath.Clean(cleanName))
|
||||
}
|
||||
|
||||
func safeArchiveTargetPath(dest, cleanName string) (string, error) {
|
||||
if cleanName == "." || strings.HasPrefix(cleanName, "../") || cleanName == ".." || filepath.IsAbs(cleanName) {
|
||||
return "", fmt.Errorf("非法文件路径: %s", cleanName)
|
||||
}
|
||||
targetPath := filepath.Join(dest, filepath.FromSlash(cleanName))
|
||||
destClean := filepath.Clean(dest)
|
||||
targetClean := filepath.Clean(targetPath)
|
||||
if targetClean != destClean && !strings.HasPrefix(targetClean, destClean+string(os.PathSeparator)) {
|
||||
return "", fmt.Errorf("非法文件路径: %s", cleanName)
|
||||
}
|
||||
return targetPath, nil
|
||||
}
|
||||
|
||||
func writeArchiveEntryFile(targetPath string, entry archiveEntry) error {
|
||||
if entry.Open == nil {
|
||||
return nil
|
||||
}
|
||||
outFile, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, entry.Mode.Perm())
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开解压文件写入失败 %s: %w", targetPath, err)
|
||||
}
|
||||
rc, err := entry.Open()
|
||||
if err != nil {
|
||||
_ = outFile.Close()
|
||||
return fmt.Errorf("读取压缩包文件失败 %s: %w", entry.Name, err)
|
||||
}
|
||||
_, copyErr := io.Copy(outFile, rc)
|
||||
closeReadErr := rc.Close()
|
||||
closeWriteErr := outFile.Close()
|
||||
if copyErr != nil {
|
||||
return fmt.Errorf("写入文件流失败 %s: %w", targetPath, copyErr)
|
||||
}
|
||||
if closeReadErr != nil {
|
||||
return closeReadErr
|
||||
}
|
||||
return closeWriteErr
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ func (m *Manager) downloadAndExtractCore(ctx context.Context, coreInput CoreInpu
|
||||
Transport: transport,
|
||||
}
|
||||
|
||||
tempFile, err := os.CreateTemp(parentDir, "download_*.zip")
|
||||
tempFile, err := os.CreateTemp(parentDir, coreArchiveTempPattern(targetUrl))
|
||||
if err != nil {
|
||||
sendEvent("error", 0, "创建临时文件失败: "+err.Error())
|
||||
return
|
||||
@@ -164,7 +164,7 @@ func (m *Manager) downloadAndExtractCore(ctx context.Context, coreInput CoreInpu
|
||||
}()
|
||||
|
||||
// 3. 执行解压,并剥离顶层文件夹
|
||||
if err := extractZipAndStripRoot(tempFilePath, tempExtractDir, func(p int, msg string) {
|
||||
if err := extractCoreArchiveAndStripRoot(tempFilePath, tempExtractDir, func(p int, msg string) {
|
||||
sendEvent("extracting", p, msg)
|
||||
}); err != nil {
|
||||
sendEvent("error", 0, "解压失败: "+err.Error())
|
||||
@@ -172,7 +172,7 @@ func (m *Manager) downloadAndExtractCore(ctx context.Context, coreInput CoreInpu
|
||||
}
|
||||
|
||||
if !m.ValidateCorePath(tempExtractDir).Valid {
|
||||
sendEvent("error", 0, fmt.Sprintf("解压后未找到浏览器可执行文件(候选:%s),请检查压缩包内容!", strings.Join(CoreExecutableCandidates(), ", ")))
|
||||
sendEvent("error", 0, fmt.Sprintf("解压后未找到当前平台可用的浏览器可执行文件(当前平台 %s,候选:%s),请检查压缩包内容!", CoreExecutablePlatform(), strings.Join(CoreExecutableCandidates(), ", ")))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -69,11 +69,11 @@ export function CoreDownloadModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FormItem label="下载地址 (ZIP)" required>
|
||||
<FormItem label="下载地址" required>
|
||||
<Input
|
||||
value={form.url}
|
||||
onChange={e => setForm(prev => ({ ...prev, url: e.target.value }))}
|
||||
placeholder="https://github.com/.../release.zip"
|
||||
placeholder="https://github.com/.../chrome-linux.tar.xz"
|
||||
disabled={progress !== null}
|
||||
/>
|
||||
<div className="text-xs text-[var(--color-text-muted)] mt-2 flex items-center justify-between bg-[var(--color-bg-muted)] p-2 rounded">
|
||||
|
||||
Reference in New Issue
Block a user