Tool: Polish Pass 1
In sow-tools, extract now behaves like a real sync by default: it overwrites changed extracted files, removes stale extracted files that no longer exist in the built archives, and normalizes extracted resource filenames to lowercase. I also made validation warn on uppercase resource filenames so mixed-case names like I_ELVENCHAIN are surfaced instead of quietly lingering. The extract command output now includes overwritten and removed counts too.
This commit is contained in:
+100
-21
@@ -16,10 +16,12 @@ import (
|
||||
)
|
||||
|
||||
type ExtractResult struct {
|
||||
ModulePath string
|
||||
HAKPaths []string
|
||||
Written int
|
||||
Skipped int
|
||||
ModulePath string
|
||||
HAKPaths []string
|
||||
Written int
|
||||
Overwritten int
|
||||
Removed int
|
||||
Skipped int
|
||||
}
|
||||
|
||||
func Extract(p *project.Project) (ExtractResult, error) {
|
||||
@@ -38,9 +40,11 @@ func Extract(p *project.Project) (ExtractResult, error) {
|
||||
var result ExtractResult
|
||||
result.ModulePath = modulePath
|
||||
var failures []error
|
||||
desired := map[string]struct{}{}
|
||||
|
||||
written, skipped, errs := extractArchiveResources(p, archive)
|
||||
written, overwritten, skipped, errs := extractArchiveResources(p, archive, desired)
|
||||
result.Written += written
|
||||
result.Overwritten += overwritten
|
||||
result.Skipped += skipped
|
||||
failures = append(failures, errs...)
|
||||
|
||||
@@ -63,21 +67,27 @@ func Extract(p *project.Project) (ExtractResult, error) {
|
||||
}
|
||||
|
||||
result.HAKPaths = append(result.HAKPaths, hakPath)
|
||||
written, skipped, errs := extractArchiveResources(p, hakArchive)
|
||||
written, overwritten, skipped, errs := extractArchiveResources(p, hakArchive, desired)
|
||||
result.Written += written
|
||||
result.Overwritten += overwritten
|
||||
result.Skipped += skipped
|
||||
failures = append(failures, errs...)
|
||||
}
|
||||
|
||||
removed, errs := cleanupStaleFiles(p, desired)
|
||||
result.Removed = removed
|
||||
failures = append(failures, errs...)
|
||||
|
||||
if len(failures) > 0 {
|
||||
return result, errors.Join(failures...)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func extractArchiveResources(p *project.Project, archive erf.Archive) (int, int, []error) {
|
||||
func extractArchiveResources(p *project.Project, archive erf.Archive, desired map[string]struct{}) (int, int, int, []error) {
|
||||
var failures []error
|
||||
writtenCount := 0
|
||||
overwrittenCount := 0
|
||||
skippedCount := 0
|
||||
|
||||
for _, resource := range archive.Resources {
|
||||
@@ -86,20 +96,24 @@ func extractArchiveResources(p *project.Project, archive erf.Archive) (int, int,
|
||||
failures = append(failures, err)
|
||||
continue
|
||||
}
|
||||
desired[target] = struct{}{}
|
||||
|
||||
written, err := writeSafely(target, data)
|
||||
state, err := writeManagedFile(target, data)
|
||||
if err != nil {
|
||||
failures = append(failures, err)
|
||||
continue
|
||||
}
|
||||
if written {
|
||||
switch state {
|
||||
case writeNew:
|
||||
writtenCount++
|
||||
} else {
|
||||
case writeOverwritten:
|
||||
overwrittenCount++
|
||||
case writeSkipped:
|
||||
skippedCount++
|
||||
}
|
||||
}
|
||||
|
||||
return writtenCount, skippedCount, failures
|
||||
return writtenCount, overwrittenCount, skippedCount, failures
|
||||
}
|
||||
|
||||
func extractedFile(p *project.Project, resource erf.Resource) (string, []byte, error) {
|
||||
@@ -107,10 +121,11 @@ func extractedFile(p *project.Project, resource erf.Resource) (string, []byte, e
|
||||
if !ok {
|
||||
return "", nil, fmt.Errorf("unsupported resource type 0x%04X for %s", resource.Type, resource.Name)
|
||||
}
|
||||
resref := strings.ToLower(resource.Name)
|
||||
|
||||
switch extension {
|
||||
case "nss":
|
||||
return filepath.Join(p.SourceDir(), "scripts", resource.Name+".nss"), resource.Data, nil
|
||||
return filepath.Join(p.SourceDir(), "scripts", resref+".nss"), resource.Data, nil
|
||||
case "utc", "utd", "ute", "uti", "utm", "utp", "uts", "utt", "utw",
|
||||
"are", "dlg", "fac", "gic", "git", "ifo", "itp", "jrl":
|
||||
document, err := gff.Read(bytes.NewReader(resource.Data))
|
||||
@@ -122,32 +137,96 @@ func extractedFile(p *project.Project, resource erf.Resource) (string, []byte, e
|
||||
return "", nil, fmt.Errorf("marshal json %s.%s: %w", resource.Name, extension, err)
|
||||
}
|
||||
formatted = append(formatted, '\n')
|
||||
return filepath.Join(p.SourceDir(), sourceSubdir(extension), resource.Name+"."+extension+".json"), formatted, nil
|
||||
return filepath.Join(p.SourceDir(), sourceSubdir(extension), resref+"."+extension+".json"), formatted, nil
|
||||
default:
|
||||
return filepath.Join(p.AssetsDir(), extension, resource.Name+"."+extension), resource.Data, nil
|
||||
return filepath.Join(p.AssetsDir(), extension, resref+"."+extension), resource.Data, nil
|
||||
}
|
||||
}
|
||||
|
||||
func writeSafely(path string, data []byte) (bool, error) {
|
||||
type writeState int
|
||||
|
||||
const (
|
||||
writeSkipped writeState = iota
|
||||
writeNew
|
||||
writeOverwritten
|
||||
)
|
||||
|
||||
func writeManagedFile(path string, data []byte) (writeState, error) {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return false, fmt.Errorf("create parent directory for %s: %w", path, err)
|
||||
return writeSkipped, fmt.Errorf("create parent directory for %s: %w", path, err)
|
||||
}
|
||||
|
||||
existing, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
if bytes.Equal(existing, data) {
|
||||
return false, nil
|
||||
return writeSkipped, nil
|
||||
}
|
||||
return false, fmt.Errorf("refusing to overwrite existing file with different contents: %s", path)
|
||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||
return writeSkipped, fmt.Errorf("overwrite %s: %w", path, err)
|
||||
}
|
||||
return writeOverwritten, nil
|
||||
}
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return false, fmt.Errorf("check existing file %s: %w", path, err)
|
||||
return writeSkipped, fmt.Errorf("check existing file %s: %w", path, err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||
return false, fmt.Errorf("write %s: %w", path, err)
|
||||
return writeSkipped, fmt.Errorf("write %s: %w", path, err)
|
||||
}
|
||||
return writeNew, nil
|
||||
}
|
||||
|
||||
func cleanupStaleFiles(p *project.Project, desired map[string]struct{}) (int, []error) {
|
||||
candidates := make([]string, 0, len(p.Inventory.SourceFiles)+len(p.Inventory.ScriptFiles)+len(p.Inventory.AssetFiles))
|
||||
for _, rel := range p.Inventory.SourceFiles {
|
||||
candidates = append(candidates, filepath.Join(p.SourceDir(), filepath.FromSlash(rel)))
|
||||
}
|
||||
for _, rel := range p.Inventory.ScriptFiles {
|
||||
candidates = append(candidates, filepath.Join(p.SourceDir(), filepath.FromSlash(rel)))
|
||||
}
|
||||
for _, rel := range p.Inventory.AssetFiles {
|
||||
candidates = append(candidates, filepath.Join(p.AssetsDir(), filepath.FromSlash(rel)))
|
||||
}
|
||||
|
||||
removed := 0
|
||||
var failures []error
|
||||
for _, path := range candidates {
|
||||
if _, keep := desired[path]; keep {
|
||||
continue
|
||||
}
|
||||
if err := os.Remove(path); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
failures = append(failures, fmt.Errorf("remove stale file %s: %w", path, err))
|
||||
continue
|
||||
}
|
||||
removed++
|
||||
cleanupEmptyParents(filepath.Dir(path), p.SourceDir(), p.AssetsDir())
|
||||
}
|
||||
return removed, failures
|
||||
}
|
||||
|
||||
func cleanupEmptyParents(dir string, roots ...string) {
|
||||
for {
|
||||
if dir == "." || dir == string(filepath.Separator) {
|
||||
return
|
||||
}
|
||||
stop := false
|
||||
for _, root := range roots {
|
||||
if dir == root {
|
||||
stop = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if stop {
|
||||
return
|
||||
}
|
||||
if err := os.Remove(dir); err != nil {
|
||||
return
|
||||
}
|
||||
dir = filepath.Dir(dir)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func sourceSubdir(extension string) string {
|
||||
|
||||
Reference in New Issue
Block a user