Parse LFS Pointers Quickly
sow-tools no longer relies on git lfs ls-files --json for planning. It now parses the checked-out LFS pointer files directly, so split sizing and content_hash use the real LFS oid and size.
This commit is contained in:
+140
-9
@@ -54,6 +54,13 @@ type assetResource struct {
|
||||
Resource erf.Resource
|
||||
Size int64
|
||||
ChangedAt time.Time
|
||||
ContentID string
|
||||
}
|
||||
|
||||
type lfsAssetInfo struct {
|
||||
OID string
|
||||
Size int64
|
||||
ObjectPath string
|
||||
}
|
||||
|
||||
type hakChunk struct {
|
||||
@@ -179,7 +186,7 @@ func planOrBuildHAKs(p *project.Project, progress ProgressFunc, writeArchives bo
|
||||
}
|
||||
|
||||
progressf(progress, "Collecting asset resources...")
|
||||
assetResources, err := collectAssetResources(p)
|
||||
assetResources, err := collectAssetResources(p, writeArchives)
|
||||
if err != nil {
|
||||
return BuildResult{}, err
|
||||
}
|
||||
@@ -556,12 +563,16 @@ func fieldStringValueForScripts(value gff.Value) (string, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func collectAssetResources(p *project.Project) ([]assetResource, error) {
|
||||
func collectAssetResources(p *project.Project, requireContent bool) ([]assetResource, error) {
|
||||
var hakResources []assetResource
|
||||
assetChangedAt, err := collectGitAssetChangeTimes(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lfsAssets, err := collectLFSAssetInfo(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
assetsRelPath, err := filepath.Rel(p.Root, p.AssetsDir())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve assets dir relative path: %w", err)
|
||||
@@ -570,7 +581,7 @@ func collectAssetResources(p *project.Project) ([]assetResource, error) {
|
||||
|
||||
for _, rel := range p.Inventory.AssetFiles {
|
||||
abs := filepath.Join(p.AssetsDir(), filepath.FromSlash(rel))
|
||||
resource, err := pathResource(abs)
|
||||
resourceInfo, err := assetResourceFromPath(abs, filepath.ToSlash(rel), lfsAssets[filepath.ToSlash(rel)], requireContent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -585,9 +596,10 @@ func collectAssetResources(p *project.Project) ([]assetResource, error) {
|
||||
}
|
||||
hakResources = append(hakResources, assetResource{
|
||||
Rel: rel,
|
||||
Resource: resource,
|
||||
Size: erf.ArchiveSize([]erf.Resource{resource}),
|
||||
Resource: resourceInfo.Resource,
|
||||
Size: erf.ArchiveSize([]erf.Resource{resourceInfo.Resource}),
|
||||
ChangedAt: changedAt,
|
||||
ContentID: resourceInfo.ContentID,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -597,6 +609,126 @@ func collectAssetResources(p *project.Project) ([]assetResource, error) {
|
||||
return hakResources, nil
|
||||
}
|
||||
|
||||
func collectLFSAssetInfo(p *project.Project) (map[string]lfsAssetInfo, error) {
|
||||
result := make(map[string]lfsAssetInfo)
|
||||
for _, rel := range p.Inventory.AssetFiles {
|
||||
rel = filepath.ToSlash(rel)
|
||||
abs := filepath.Join(p.AssetsDir(), filepath.FromSlash(rel))
|
||||
info, ok, err := parseLFSPointerFile(abs, p.Root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
result[rel] = info
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseLFSPointerFile(path, repoRoot string) (lfsAssetInfo, bool, error) {
|
||||
entry, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return lfsAssetInfo{}, false, fmt.Errorf("stat %s: %w", path, err)
|
||||
}
|
||||
if entry.Mode()&os.ModeSymlink != 0 || entry.Size() > 1024 {
|
||||
return lfsAssetInfo{}, false, nil
|
||||
}
|
||||
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return lfsAssetInfo{}, false, fmt.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
text := strings.ReplaceAll(string(raw), "\r\n", "\n")
|
||||
if !strings.HasPrefix(text, "version https://git-lfs.github.com/spec/v1\n") {
|
||||
return lfsAssetInfo{}, false, nil
|
||||
}
|
||||
|
||||
info := lfsAssetInfo{}
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
switch {
|
||||
case strings.HasPrefix(line, "oid sha256:"):
|
||||
info.OID = strings.TrimSpace(strings.TrimPrefix(line, "oid sha256:"))
|
||||
case strings.HasPrefix(line, "size "):
|
||||
size, parseErr := strconv.ParseInt(strings.TrimSpace(strings.TrimPrefix(line, "size ")), 10, 64)
|
||||
if parseErr != nil {
|
||||
return lfsAssetInfo{}, false, fmt.Errorf("parse LFS pointer size in %s: %w", path, parseErr)
|
||||
}
|
||||
info.Size = size
|
||||
}
|
||||
}
|
||||
if info.OID == "" || info.Size <= 0 {
|
||||
return lfsAssetInfo{}, false, nil
|
||||
}
|
||||
if len(info.OID) >= 4 {
|
||||
info.ObjectPath = filepath.Join(repoRoot, ".git", "lfs", "objects", info.OID[:2], info.OID[2:4], info.OID)
|
||||
}
|
||||
return info, true, nil
|
||||
}
|
||||
|
||||
func assetResourceFromPath(path, rel string, lfsInfo lfsAssetInfo, requireContent bool) (assetResource, error) {
|
||||
if lfsInfo.OID != "" {
|
||||
resource, err := lfsPathResource(path, lfsInfo, requireContent)
|
||||
if err != nil {
|
||||
return assetResource{}, err
|
||||
}
|
||||
return assetResource{
|
||||
Rel: rel,
|
||||
Resource: resource,
|
||||
ContentID: "lfs:" + lfsInfo.OID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
resource, err := pathResource(path)
|
||||
if err != nil {
|
||||
return assetResource{}, err
|
||||
}
|
||||
fileHash, err := fileSHA256(path)
|
||||
if err != nil {
|
||||
return assetResource{}, err
|
||||
}
|
||||
return assetResource{
|
||||
Rel: rel,
|
||||
Resource: resource,
|
||||
ContentID: "file:" + fileHash,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func lfsPathResource(path string, info lfsAssetInfo, requireContent bool) (erf.Resource, error) {
|
||||
extension := strings.TrimPrefix(strings.ToLower(filepath.Ext(path)), ".")
|
||||
resourceType, ok := erf.HAKResourceTypeForExtension(extension)
|
||||
if !ok {
|
||||
return erf.Resource{}, fmt.Errorf("unsupported HAK resource extension %q", filepath.Ext(path))
|
||||
}
|
||||
name := strings.ToLower(strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)))
|
||||
sourcePath := ""
|
||||
if requireContent {
|
||||
if info.ObjectPath == "" {
|
||||
return erf.Resource{}, fmt.Errorf("missing downloaded LFS object for %s", path)
|
||||
}
|
||||
if _, err := os.Stat(info.ObjectPath); err != nil {
|
||||
return erf.Resource{}, fmt.Errorf("missing downloaded LFS object for %s: %w", path, err)
|
||||
}
|
||||
sourcePath = info.ObjectPath
|
||||
}
|
||||
return erf.Resource{
|
||||
Name: name,
|
||||
Type: resourceType,
|
||||
SourcePath: sourcePath,
|
||||
Size: info.Size,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func fileSHA256(path string) (string, error) {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read %s: %w", path, err)
|
||||
}
|
||||
sum := sha256.Sum256(raw)
|
||||
return fmt.Sprintf("%x", sum[:]), nil
|
||||
}
|
||||
|
||||
func collectGitAssetChangeTimes(p *project.Project) (map[string]time.Time, error) {
|
||||
result := make(map[string]time.Time, len(p.Inventory.AssetFiles))
|
||||
if len(p.Inventory.AssetFiles) == 0 {
|
||||
@@ -960,9 +1092,8 @@ func chunkContentHash(chunk hakChunk) string {
|
||||
fmt.Fprintf(sum, "asset:%s\n", asset.Rel)
|
||||
fmt.Fprintf(sum, "resref:%s\n", asset.Resource.Name)
|
||||
fmt.Fprintf(sum, "restype:%d\n", asset.Resource.Type)
|
||||
fmt.Fprintf(sum, "datasize:%d\n", len(asset.Resource.Data))
|
||||
_, _ = sum.Write(asset.Resource.Data)
|
||||
_, _ = sum.Write([]byte{'\n'})
|
||||
fmt.Fprintf(sum, "datasize:%d\n", asset.Resource.Size)
|
||||
fmt.Fprintf(sum, "content:%s\n", asset.ContentID)
|
||||
}
|
||||
return fmt.Sprintf("%x", sum.Sum(nil))
|
||||
}
|
||||
@@ -1086,7 +1217,7 @@ func plannedModuleHAKOrder(p *project.Project) ([]string, error) {
|
||||
return append([]string(nil), manifest.ModuleHAKs...), nil
|
||||
}
|
||||
|
||||
assetResources, err := collectAssetResources(p)
|
||||
assetResources, err := collectAssetResources(p, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user