Files
Work/Backend/internal/posixproj/projector_scan.go
T
2026-06-29 04:29:05 +01:00

190 lines
5.0 KiB
Go

// Path: Backend/internal/posixproj/projector_scan.go
package posixproj
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
)
func ScanRoot(root string) ([]Node, error) {
rootPath := strings.TrimSpace(root)
if rootPath == "" {
return nil, nil
}
info, err := os.Stat(rootPath)
if err != nil {
return nil, fmt.Errorf("stat POSIX root: %w", err)
}
if !info.IsDir() {
return nil, fmt.Errorf("POSIX root is not a directory: %s", rootPath)
}
rootScope, err := loadRootScope(rootPath)
if err != nil {
return nil, err
}
nodes := []Node{{
Path: rootProjectionPath,
ParentPath: nil,
Name: filepath.Base(rootPath),
Depth: 0,
NodeKind: NodeKindDirectory,
LogicalType: "tenant_root",
InstallationID: rootScope.InstallationID,
OrganizationID: rootScope.OrganizationID,
OrganizationSlug: rootScope.OrganizationSlug,
}}
err = filepath.WalkDir(rootPath, func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if path == rootPath {
return nil
}
relPath, err := filepath.Rel(rootPath, path)
if err != nil {
return err
}
relPath = filepath.ToSlash(relPath)
if relPath == "." {
return nil
}
node, err := buildNode(rootPath, relPath, entry, rootScope)
if err != nil {
return err
}
nodes = append(nodes, node)
return nil
})
if err != nil {
return nil, fmt.Errorf("scan POSIX root: %w", err)
}
return nodes, nil
}
func loadRootScope(rootPath string) (Scope, error) {
settingsPath := filepath.Join(rootPath, "settings.json")
content, err := os.ReadFile(settingsPath)
if err != nil {
if errorsIsNotExist(err) {
return Scope{}, nil
}
return Scope{}, fmt.Errorf("read root settings.json: %w", err)
}
var payload map[string]any
if err := json.Unmarshal(content, &payload); err != nil {
return Scope{}, fmt.Errorf("decode root settings.json: %w", err)
}
installation, _ := payload["installation"].(map[string]any)
organization, _ := payload["organization"].(map[string]any)
return Scope{
InstallationID: stringValue(installation["id"]),
OrganizationID: stringValue(organization["id"]),
OrganizationSlug: stringValue(organization["slug"]),
}, nil
}
func buildNode(rootPath, relPath string, entry fs.DirEntry, rootScope Scope) (Node, error) {
scope := deriveScope(relPath, rootScope)
parentPath := projectionParentPath(relPath)
logicalType, fileRole := classifyPath(relPath, entry.IsDir())
node := Node{
Path: relPath,
ParentPath: parentPath,
Name: entry.Name(),
Depth: strings.Count(relPath, "/") + 1,
NodeKind: NodeKindDirectory,
LogicalType: logicalType,
FileRole: fileRole,
InstallationID: scope.InstallationID,
OrganizationID: scope.OrganizationID,
OrganizationSlug: scope.OrganizationSlug,
DepartmentSlug: scope.DepartmentSlug,
TeamSlug: scope.TeamSlug,
ProjectSlug: scope.ProjectSlug,
PersonalSlug: scope.PersonalSlug,
}
if entry.IsDir() {
return node, nil
}
absPath := filepath.Join(rootPath, filepath.FromSlash(relPath))
content, err := os.ReadFile(absPath)
if err != nil {
return Node{}, fmt.Errorf("read POSIX file %s: %w", relPath, err)
}
hash := sha256.Sum256(content)
node.NodeKind = NodeKindFile
node.SizeBytes = int64(len(content))
node.Checksum = hex.EncodeToString(hash[:])
if strings.EqualFold(filepath.Ext(entry.Name()), ".json") {
var payload map[string]any
if err := json.Unmarshal(content, &payload); err == nil {
jsonContent, err := json.Marshal(payload)
if err != nil {
return Node{}, fmt.Errorf("remarshal POSIX file %s: %w", relPath, err)
}
node.ContentJSON = jsonContent
node.ResourceID = stringValue(payload["id"])
node.ResourceName = stringValue(payload["name"])
node.ResourceSlug = stringValue(payload["slug"])
if node.ResourceID == "" && fileRole == "settings" && logicalType == "tenant" {
installation, _ := payload["installation"].(map[string]any)
organization, _ := payload["organization"].(map[string]any)
node.ResourceID = stringValue(installation["id"])
node.ResourceName = stringValue(installation["name"])
node.InstallationID = stringValue(installation["id"])
node.OrganizationID = stringValue(organization["id"])
node.OrganizationSlug = firstNonEmpty(node.OrganizationSlug, stringValue(organization["slug"]))
}
if node.ResourceID == "" && fileRole == "users" {
node.ResourceName = firstNonEmpty(node.ResourceName, parentEntityName(logicalType, scope))
}
}
}
if node.ResourceSlug == "" {
node.ResourceSlug = inferredResourceSlug(logicalType, scope)
}
return node, nil
}
func summarizeNodes(nodes []Node) RebuildSummary {
summary := RebuildSummary{TotalNodes: len(nodes)}
for _, node := range nodes {
switch node.NodeKind {
case NodeKindDirectory:
summary.DirectoryCount++
case NodeKindFile:
summary.FileCount++
}
}
return summary
}
func errorsIsNotExist(err error) bool {
return err != nil && os.IsNotExist(err)
}