DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,579 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
|
||||
"pvfine/internal/pvf"
|
||||
pvfversion "pvfine/internal/version"
|
||||
)
|
||||
|
||||
const (
|
||||
ImportModeText = "text"
|
||||
ImportModeRaw = "raw"
|
||||
maxImportPreviewEntries = 500
|
||||
)
|
||||
|
||||
// ImportResult describes one atomically applied import operation.
|
||||
type ImportResult struct {
|
||||
TargetDir string `json:"targetDir"`
|
||||
Mode string `json:"mode"`
|
||||
ImportedCount int `json:"importedCount"`
|
||||
OverwrittenCount int `json:"overwrittenCount"`
|
||||
ChangedPaths []string `json:"changedPaths,omitempty"`
|
||||
}
|
||||
|
||||
// ImportPreview describes the validated changes without modifying the live
|
||||
// archive. Entries is capped for large directory imports; the counters remain
|
||||
// complete.
|
||||
type ImportPreview struct {
|
||||
TargetDir string `json:"targetDir"`
|
||||
Mode string `json:"mode"`
|
||||
TotalFiles int `json:"totalFiles"`
|
||||
ImportedCount int `json:"importedCount"`
|
||||
OverwrittenCount int `json:"overwrittenCount"`
|
||||
Entries []*ImportPreviewEntry `json:"entries"`
|
||||
EntriesTruncated bool `json:"entriesTruncated"`
|
||||
ConvertedCount int `json:"convertedCount"`
|
||||
}
|
||||
|
||||
// ImportPreviewEntry is one validated source-to-archive mapping.
|
||||
type ImportPreviewEntry struct {
|
||||
SourcePath string `json:"sourcePath"`
|
||||
TargetPath string `json:"targetPath"`
|
||||
DataType int32 `json:"dataType"`
|
||||
Size int64 `json:"size"`
|
||||
Overwrite bool `json:"overwrite"`
|
||||
SourceEncoding string `json:"sourceEncoding"`
|
||||
}
|
||||
|
||||
type importFile struct {
|
||||
sourcePath string
|
||||
targetPath string
|
||||
data []byte
|
||||
sourceEncoding string
|
||||
text string
|
||||
dataType int32
|
||||
}
|
||||
|
||||
type importSelection struct {
|
||||
path string
|
||||
isDir bool
|
||||
}
|
||||
|
||||
// ImportFilesDialog opens a native multi-selection picker and imports the
|
||||
// selected files or directories into targetDir.
|
||||
func (s *ArchiveService) ImportFilesDialog(targetDir, mode string) (*ImportResult, error) {
|
||||
paths, err := s.SelectImportFilesDialog()
|
||||
if err != nil || len(paths) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
return s.ImportFiles(paths, targetDir, mode)
|
||||
}
|
||||
|
||||
// SelectImportFilesDialog opens the native multi-selection picker without
|
||||
// changing the current archive. The returned paths can be previewed first.
|
||||
func (s *ArchiveService) SelectImportFilesDialog() ([]string, error) {
|
||||
return application.Get().Dialog.OpenFile().
|
||||
CanChooseFiles(true).
|
||||
CanChooseDirectories(true).
|
||||
AddFilter("所有文件", "*").
|
||||
SetTitle("导入文件").
|
||||
PromptForMultipleSelection()
|
||||
}
|
||||
|
||||
// PreviewImport validates and maps sourcePaths without modifying the live
|
||||
// archive. It also stages the files on an isolated archive to catch encoding
|
||||
// and index-building errors before the user confirms the operation.
|
||||
func (s *ArchiveService) PreviewImport(sourcePaths []string, targetDir, mode string) (*ImportPreview, error) {
|
||||
files, targetDir, mode, err := prepareImportFiles(sourcePaths, targetDir, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return nil, errorsNoImportFiles()
|
||||
}
|
||||
|
||||
finishTask := s.c.archiveTasks.begin()
|
||||
defer finishTask()
|
||||
s.c.mu.RLock()
|
||||
a := s.c.archive
|
||||
if a == nil {
|
||||
s.c.mu.RUnlock()
|
||||
return nil, ErrNoArchive
|
||||
}
|
||||
if err := s.c.ensureVersionReadyLocked(); err != nil {
|
||||
s.c.mu.RUnlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
preview := &ImportPreview{
|
||||
TargetDir: targetDir,
|
||||
Mode: mode,
|
||||
TotalFiles: len(files),
|
||||
Entries: []*ImportPreviewEntry{},
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.sourceEncoding != "" && file.sourceEncoding != "UTF-8" {
|
||||
preview.ConvertedCount++
|
||||
}
|
||||
_, overwrite := a.Find(file.targetPath)
|
||||
if overwrite {
|
||||
preview.OverwrittenCount++
|
||||
} else {
|
||||
preview.ImportedCount++
|
||||
}
|
||||
if len(preview.Entries) < maxImportPreviewEntries {
|
||||
preview.Entries = append(preview.Entries, &ImportPreviewEntry{
|
||||
SourcePath: file.sourcePath,
|
||||
TargetPath: file.targetPath,
|
||||
DataType: file.dataType,
|
||||
Size: int64(len(file.data)),
|
||||
Overwrite: overwrite,
|
||||
SourceEncoding: file.sourceEncoding,
|
||||
})
|
||||
} else {
|
||||
preview.EntriesTruncated = true
|
||||
}
|
||||
}
|
||||
|
||||
stage := a.CloneForBatch()
|
||||
if _, err := applyPreparedImport(stage, files, mode); err != nil {
|
||||
s.c.mu.RUnlock()
|
||||
return nil, err
|
||||
}
|
||||
if stage.FileCount() < largeArchiveIndexThreshold {
|
||||
if _, _, err := buildIndex(stage); err != nil {
|
||||
s.c.mu.RUnlock()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if s.c.versionRepo != nil {
|
||||
targetPaths := make([]string, 0, len(files))
|
||||
for _, file := range files {
|
||||
targetPaths = append(targetPaths, file.targetPath)
|
||||
}
|
||||
if _, err := pvfversion.ContentSnapshotFromArchive(stage, targetPaths); err != nil {
|
||||
s.c.mu.RUnlock()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
s.c.mu.RUnlock()
|
||||
return preview, nil
|
||||
}
|
||||
|
||||
// ImportFiles imports sourcePaths into targetDir. The source paths may be
|
||||
// files or directories. All source data is read and staged before the live
|
||||
// archive is replaced, so an error leaves the current archive unchanged.
|
||||
func (s *ArchiveService) ImportFiles(sourcePaths []string, targetDir, mode string) (*ImportResult, error) {
|
||||
files, targetDir, mode, err := prepareImportFiles(sourcePaths, targetDir, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return nil, errorsNoImportFiles()
|
||||
}
|
||||
|
||||
return s.importPrepared(files, targetDir, mode)
|
||||
}
|
||||
|
||||
func (s *ArchiveService) importPrepared(files []importFile, targetDir, mode string) (*ImportResult, error) {
|
||||
return s.importPreparedRevision(files, targetDir, mode, "", 0)
|
||||
}
|
||||
func (s *ArchiveService) importPreparedRevision(files []importFile, targetDir, mode, workspace string, expectedRevision uint64) (*ImportResult, error) {
|
||||
return s.importPreparedChecked(files, targetDir, mode, "导入文件", func() error {
|
||||
if workspace != "" && (s.c.mcpWorkspaceID != workspace || s.c.batchRevision != expectedRevision) {
|
||||
return fmt.Errorf("STALE_IMPORT")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// check runs under the core lock, atomically with taking the staging snapshot.
|
||||
func (s *ArchiveService) importPreparedChecked(files []importFile, targetDir, mode, label string, check func() error) (*ImportResult, error) {
|
||||
var err error
|
||||
s.c.mu.Lock()
|
||||
if err := check(); err != nil {
|
||||
s.c.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a := s.c.archive
|
||||
if a == nil {
|
||||
s.c.mu.Unlock()
|
||||
return nil, ErrNoArchive
|
||||
}
|
||||
if err := s.c.ensureVersionReadyLocked(); err != nil {
|
||||
s.c.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
targetPaths := make([]string, 0, len(files))
|
||||
for _, file := range files {
|
||||
targetPaths = append(targetPaths, file.targetPath)
|
||||
}
|
||||
|
||||
var before pvfversion.ContentSnapshot
|
||||
if s.c.versionRepo != nil {
|
||||
before, err = pvfversion.ContentSnapshotFromArchive(a, targetPaths)
|
||||
if err != nil {
|
||||
s.c.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
stage := a.CloneForBatch()
|
||||
result, err := applyPreparedImport(stage, files, mode)
|
||||
if err != nil {
|
||||
s.c.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
result.TargetDir = targetDir
|
||||
result.Mode = mode
|
||||
if len(stage.MutationsSince(0).Files) == 0 {
|
||||
// An identical overwrite has no projection or semantic work to publish.
|
||||
// In particular, do not reset a ready index with an empty mutation queue.
|
||||
s.c.mu.Unlock()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// The staged archive is isolated, so prepare its directory/SQLite
|
||||
// projection without holding the core lock. A concurrent archive mutation
|
||||
// invalidates this import before it is installed.
|
||||
revision := s.c.batchRevision
|
||||
repo, loadID, headID := s.c.versionRepo, s.c.versionLoadID, s.c.versionHead.ID
|
||||
s.c.batchRevision++
|
||||
incrementalSQLite := s.c.diskIndex != nil && s.c.diskIndex.entries != nil
|
||||
s.c.mu.Unlock()
|
||||
// A live SQLite projection can be updated in place after the revision
|
||||
// check. Do not build a second database merely to overwrite existing files.
|
||||
var prepared *preparedArchiveIndexes
|
||||
var prepareErr error
|
||||
if !incrementalSQLite {
|
||||
prepared, prepareErr = prepareArchiveDerivedIndexes(stage)
|
||||
}
|
||||
s.c.mu.Lock()
|
||||
if prepareErr != nil {
|
||||
closePreparedArchiveIndexes(prepared)
|
||||
s.c.mu.Unlock()
|
||||
return nil, prepareErr
|
||||
}
|
||||
if s.c.archive != a || s.c.batchRevision != revision+1 || s.c.versionRepo != repo || s.c.versionLoadID != loadID || s.c.versionHead.ID != headID {
|
||||
closePreparedArchiveIndexes(prepared)
|
||||
s.c.mu.Unlock()
|
||||
return nil, ErrBatchPlanStale
|
||||
}
|
||||
var after pvfversion.ContentSnapshot
|
||||
if s.c.versionRepo != nil {
|
||||
var snapshotErr error
|
||||
after, snapshotErr = pvfversion.ContentSnapshotFromArchive(stage, targetPaths)
|
||||
if snapshotErr != nil {
|
||||
closePreparedArchiveIndexes(prepared)
|
||||
s.c.mu.Unlock()
|
||||
return nil, snapshotErr
|
||||
}
|
||||
}
|
||||
var installErr error
|
||||
if incrementalSQLite {
|
||||
installErr = s.c.rebuildArchiveIndexesLocked(stage)
|
||||
} else {
|
||||
installErr = s.c.installPreparedArchiveIndexesLocked(stage, prepared, true)
|
||||
}
|
||||
if installErr != nil {
|
||||
closePreparedArchiveIndexes(prepared)
|
||||
s.c.mu.Unlock()
|
||||
return nil, installErr
|
||||
}
|
||||
if s.c.versionRepo != nil {
|
||||
if err := s.c.recordVersionMutationLocked(label, before, after); err != nil {
|
||||
s.c.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
mutationSummary := stage.MutationsSince(0)
|
||||
stage.ClearMutations()
|
||||
info := stage.Info()
|
||||
versioned := s.c.versionRepo != nil
|
||||
s.c.mu.Unlock()
|
||||
|
||||
if err := s.c.scheduleArchiveMutations(stage, mutationSummary); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
emitEvent("archive:changed", info)
|
||||
emitEvent("archive:advanced-search-stale", map[string]any{"import": true})
|
||||
if versioned {
|
||||
emitVersionState(s.c, "files-imported")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func errorsNoImportFiles() error {
|
||||
return fmt.Errorf("没有可导入的文件")
|
||||
}
|
||||
|
||||
func prepareImportFiles(sourcePaths []string, targetDir, mode string) ([]importFile, string, string, error) {
|
||||
var err error
|
||||
targetDir, err = normalizeImportTargetDir(targetDir)
|
||||
if err != nil {
|
||||
return nil, "", "", err
|
||||
}
|
||||
mode, err = normalizeImportMode(mode)
|
||||
if err != nil {
|
||||
return nil, "", "", err
|
||||
}
|
||||
files, err := collectImportFiles(sourcePaths, targetDir)
|
||||
if err != nil {
|
||||
return nil, "", "", err
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return files, targetDir, mode, nil
|
||||
}
|
||||
if err := readImportFiles(files, mode); err != nil {
|
||||
return nil, "", "", err
|
||||
}
|
||||
return files, targetDir, mode, nil
|
||||
}
|
||||
|
||||
func normalizeImportMode(mode string) (string, error) {
|
||||
mode = strings.ToLower(strings.TrimSpace(mode))
|
||||
if mode == "" {
|
||||
mode = ImportModeText
|
||||
}
|
||||
if mode != ImportModeText && mode != ImportModeRaw {
|
||||
return "", fmt.Errorf("不支持的导入模式: %q", mode)
|
||||
}
|
||||
return mode, nil
|
||||
}
|
||||
|
||||
func normalizeImportTargetDir(raw string) (string, error) {
|
||||
dir := strings.Trim(strings.ReplaceAll(strings.TrimSpace(raw), "\\", "/"), "/")
|
||||
if dir == "" {
|
||||
return "", nil
|
||||
}
|
||||
return normalizeNewFilePath(dir)
|
||||
}
|
||||
|
||||
func collectImportFiles(sourcePaths []string, targetDir string) ([]importFile, error) {
|
||||
return collectImportFilesLimited(sourcePaths, targetDir, 0)
|
||||
}
|
||||
func collectImportFilesLimited(sourcePaths []string, targetDir string, maxFiles int) ([]importFile, error) {
|
||||
selections := make([]importSelection, 0, len(sourcePaths))
|
||||
seenSelections := make(map[string]struct{}, len(sourcePaths))
|
||||
for _, rawPath := range sourcePaths {
|
||||
rawPath = strings.TrimSpace(rawPath)
|
||||
if rawPath == "" {
|
||||
continue
|
||||
}
|
||||
path, err := filepath.Abs(filepath.Clean(rawPath))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析来源路径 %q 失败: %w", rawPath, err)
|
||||
}
|
||||
if _, exists := seenSelections[path]; exists {
|
||||
continue
|
||||
}
|
||||
seenSelections[path] = struct{}{}
|
||||
|
||||
info, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取来源路径 %q 失败: %w", rawPath, err)
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
return nil, fmt.Errorf("不支持符号链接来源: %q", rawPath)
|
||||
}
|
||||
if info.IsDir() {
|
||||
selections = append(selections, importSelection{path: path, isDir: true})
|
||||
continue
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("来源不是普通文件或目录: %q", rawPath)
|
||||
}
|
||||
selections = append(selections, importSelection{path: path})
|
||||
}
|
||||
|
||||
sort.Slice(selections, func(i, j int) bool {
|
||||
return selections[i].path < selections[j].path
|
||||
})
|
||||
selectedDirs := make([]string, 0)
|
||||
filtered := selections[:0]
|
||||
for _, selection := range selections {
|
||||
redundant := false
|
||||
for _, dir := range selectedDirs {
|
||||
if selection.path != dir && isPathWithin(dir, selection.path) {
|
||||
redundant = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if redundant {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, selection)
|
||||
if selection.isDir {
|
||||
selectedDirs = append(selectedDirs, selection.path)
|
||||
}
|
||||
}
|
||||
|
||||
files := make([]importFile, 0)
|
||||
seenSources := make(map[string]struct{})
|
||||
appendFile := func(sourcePath, relativePath string) error {
|
||||
if _, exists := seenSources[sourcePath]; exists {
|
||||
return nil
|
||||
}
|
||||
seenSources[sourcePath] = struct{}{}
|
||||
targetPath := targetDir
|
||||
if targetPath != "" {
|
||||
targetPath += "/"
|
||||
}
|
||||
targetPath += filepath.ToSlash(relativePath)
|
||||
targetPath, err := normalizeNewFilePath(targetPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("来源 %q 映射到归档路径失败: %w", sourcePath, err)
|
||||
}
|
||||
if maxFiles > 0 && len(files) >= maxFiles {
|
||||
return fmt.Errorf("IMPORT_LIMIT: 导入文件过多")
|
||||
}
|
||||
files = append(files, importFile{sourcePath: sourcePath, targetPath: targetPath})
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, selection := range filtered {
|
||||
if !selection.isDir {
|
||||
if err := appendFile(selection.path, filepath.Base(selection.path)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
directoryName := filepath.Base(selection.path)
|
||||
err := filepath.WalkDir(selection.path, func(path string, entry fs.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if entry.Type()&os.ModeSymlink != 0 {
|
||||
if entry.IsDir() {
|
||||
return fs.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
info, infoErr := entry.Info()
|
||||
if infoErr != nil {
|
||||
return infoErr
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return nil
|
||||
}
|
||||
relative, relErr := filepath.Rel(selection.path, path)
|
||||
if relErr != nil {
|
||||
return relErr
|
||||
}
|
||||
return appendFile(path, filepath.Join(directoryName, relative))
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描目录 %q 失败: %w", selection.path, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return []importFile{}, nil
|
||||
}
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
left := strings.ToLower(strings.ReplaceAll(files[i].targetPath, "\\", "/"))
|
||||
right := strings.ToLower(strings.ReplaceAll(files[j].targetPath, "\\", "/"))
|
||||
if left != right {
|
||||
return left < right
|
||||
}
|
||||
return files[i].sourcePath < files[j].sourcePath
|
||||
})
|
||||
seenTargets := make(map[string]string, len(files))
|
||||
for _, file := range files {
|
||||
key := strings.ToLower(file.targetPath)
|
||||
if previous, exists := seenTargets[key]; exists {
|
||||
return nil, fmt.Errorf("批次内归档路径冲突: %q 与 %q 都映射到 %q", previous, file.sourcePath, file.targetPath)
|
||||
}
|
||||
seenTargets[key] = file.sourcePath
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func isPathWithin(root, candidate string) bool {
|
||||
relative, err := filepath.Rel(root, candidate)
|
||||
if err != nil || relative == "." || relative == ".." || filepath.IsAbs(relative) {
|
||||
return false
|
||||
}
|
||||
prefix := ".." + string(filepath.Separator)
|
||||
return !strings.HasPrefix(relative, prefix)
|
||||
}
|
||||
|
||||
func readImportFiles(files []importFile, mode string) error {
|
||||
for index := range files {
|
||||
file := &files[index]
|
||||
data, err := os.ReadFile(file.sourcePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取 %q 失败: %w", file.sourcePath, err)
|
||||
}
|
||||
file.data = data
|
||||
file.dataType = importDataType(file.targetPath)
|
||||
if mode == ImportModeText {
|
||||
if err := file.decodeText(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func importDataType(path string) int32 {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
if ext == ".str" || ext == ".nut" {
|
||||
return pvf.TypeUnicode
|
||||
}
|
||||
return pvf.TypeScript
|
||||
}
|
||||
|
||||
func applyImportFile(a *pvf.Archive, file importFile, mode string, index int32) error {
|
||||
if mode == ImportModeText {
|
||||
return a.SetText(index, file.text)
|
||||
}
|
||||
return a.SetRawBytes(index, file.data)
|
||||
}
|
||||
|
||||
func addImportFile(a *pvf.Archive, file importFile, mode string) error {
|
||||
if mode == ImportModeText {
|
||||
_, err := a.AddFileText(file.targetPath, file.text, file.dataType)
|
||||
return err
|
||||
}
|
||||
a.AddFile(file.targetPath, file.data, file.dataType)
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyPreparedImport(a *pvf.Archive, files []importFile, mode string) (*ImportResult, error) {
|
||||
result := &ImportResult{ChangedPaths: []string{}}
|
||||
for _, file := range files {
|
||||
index, exists := a.Find(file.targetPath)
|
||||
if exists {
|
||||
if err := a.SetDataType(index, file.dataType); err != nil {
|
||||
return nil, fmt.Errorf("设置 %q 类型失败: %w", file.targetPath, err)
|
||||
}
|
||||
if err := applyImportFile(a, file, mode, index); err != nil {
|
||||
return nil, fmt.Errorf("导入 %q 到 %q 失败: %w", file.sourcePath, file.targetPath, err)
|
||||
}
|
||||
result.OverwrittenCount++
|
||||
result.ChangedPaths = append(result.ChangedPaths, a.Path(index))
|
||||
continue
|
||||
}
|
||||
|
||||
if err := addImportFile(a, file, mode); err != nil {
|
||||
return nil, fmt.Errorf("导入 %q 到 %q 失败: %w", file.sourcePath, file.targetPath, err)
|
||||
}
|
||||
result.ImportedCount++
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
Reference in new issue
Block a user