// 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) }