Scan nested Java aggregate roots

This commit is contained in:
John Burwell 2026-06-02 16:24:25 -05:00
parent 6e728724bb
commit ca21654b44
2 changed files with 128 additions and 52 deletions

View File

@ -733,47 +733,49 @@ enum JavaContentScanner {
let fileManager = FileManager.default let fileManager = FileManager.default
var discoveredItems: [MinecraftContentItem] = [] var discoveredItems: [MinecraftContentItem] = []
let savesRootURL = existingDirectory( for scanRootURL in contentScanRoots(for: searchRootURL, fileManager: fileManager) {
named: "saves", let savesRootURL = existingDirectory(
in: searchRootURL, named: "saves",
fileManager: fileManager in: scanRootURL,
) ?? searchRootURL
let worldItems = try discoverWorlds(in: savesRootURL, fileManager: fileManager)
discoveredItems.append(contentsOf: worldItems)
if let resourcePacksURL = existingDirectory(named: "resourcepacks", in: searchRootURL, fileManager: fileManager) {
let resourcePackItems = try discoverResourcePacks(in: resourcePacksURL, fileManager: fileManager)
discoveredItems.append(contentsOf: resourcePackItems)
}
if let dataPacksURL = existingDirectory(named: "datapacks", in: searchRootURL, fileManager: fileManager) {
discoveredItems.append(contentsOf: try discoverJavaPackages(
in: dataPacksURL,
contentKind: .dataPack,
platformType: .dataPack,
packageExtension: "zip",
fileManager: fileManager fileManager: fileManager
)) ) ?? scanRootURL
} let worldItems = try discoverWorlds(in: savesRootURL, fileManager: fileManager)
discoveredItems.append(contentsOf: worldItems)
if let shaderPacksURL = existingDirectory(named: "shaderpacks", in: searchRootURL, fileManager: fileManager) { if let resourcePacksURL = existingDirectory(named: "resourcepacks", in: scanRootURL, fileManager: fileManager) {
discoveredItems.append(contentsOf: try discoverJavaPackages( let resourcePackItems = try discoverResourcePacks(in: resourcePacksURL, fileManager: fileManager)
in: shaderPacksURL, discoveredItems.append(contentsOf: resourcePackItems)
contentKind: .shaderPack, }
platformType: .shaderPack,
packageExtension: "zip",
fileManager: fileManager
))
}
if let modsURL = existingDirectory(named: "mods", in: searchRootURL, fileManager: fileManager) { if let dataPacksURL = existingDirectory(named: "datapacks", in: scanRootURL, fileManager: fileManager) {
discoveredItems.append(contentsOf: try discoverJavaPackages( discoveredItems.append(contentsOf: try discoverJavaPackages(
in: modsURL, in: dataPacksURL,
contentKind: .mod, contentKind: .dataPack,
platformType: .mod, platformType: .dataPack,
packageExtension: "jar", packageExtension: "zip",
fileManager: fileManager fileManager: fileManager
)) ))
}
if let shaderPacksURL = existingDirectory(named: "shaderpacks", in: scanRootURL, fileManager: fileManager) {
discoveredItems.append(contentsOf: try discoverJavaPackages(
in: shaderPacksURL,
contentKind: .shaderPack,
platformType: .shaderPack,
packageExtension: "zip",
fileManager: fileManager
))
}
if let modsURL = existingDirectory(named: "mods", in: scanRootURL, fileManager: fileManager) {
discoveredItems.append(contentsOf: try discoverJavaPackages(
in: modsURL,
contentKind: .mod,
platformType: .mod,
packageExtension: "jar",
fileManager: fileManager
))
}
} }
discoveredItems.sort(by: WorldScanner.sortItems) discoveredItems.sort(by: WorldScanner.sortItems)
@ -806,21 +808,32 @@ enum JavaContentScanner {
nonisolated static func collectionSnapshots(in sourceRootURL: URL) -> [CollectionSnapshot] { nonisolated static func collectionSnapshots(in sourceRootURL: URL) -> [CollectionSnapshot] {
let fileManager = FileManager.default let fileManager = FileManager.default
let candidateRoots = [ var snapshots: [CollectionSnapshot] = []
existingDirectory(named: "saves", in: sourceRootURL, fileManager: fileManager), for scanRootURL in contentScanRoots(for: sourceRootURL, fileManager: fileManager) {
existingDirectory(named: "resourcepacks", in: sourceRootURL, fileManager: fileManager), let candidateRoots = [
existingDirectory(named: "datapacks", in: sourceRootURL, fileManager: fileManager), existingDirectory(named: "saves", in: scanRootURL, fileManager: fileManager),
existingDirectory(named: "shaderpacks", in: sourceRootURL, fileManager: fileManager), existingDirectory(named: "resourcepacks", in: scanRootURL, fileManager: fileManager),
existingDirectory(named: "mods", in: sourceRootURL, fileManager: fileManager) existingDirectory(named: "datapacks", in: scanRootURL, fileManager: fileManager),
] existingDirectory(named: "shaderpacks", in: scanRootURL, fileManager: fileManager),
existingDirectory(named: "mods", in: scanRootURL, fileManager: fileManager)
]
return candidateRoots.compactMap { collectionURL in for collectionURL in candidateRoots {
guard let collectionURL else { guard let collectionURL else {
return nil continue
}
if let snapshot = collectionSnapshot(
for: collectionURL,
sourceRootURL: sourceRootURL,
fileManager: fileManager
) {
snapshots.append(snapshot)
}
} }
return collectionSnapshot(for: collectionURL, fileManager: fileManager)
} }
return snapshots
} }
nonisolated private static func discoverWorlds(in savesRootURL: URL, fileManager: FileManager) throws -> [MinecraftContentItem] { nonisolated private static func discoverWorlds(in savesRootURL: URL, fileManager: FileManager) throws -> [MinecraftContentItem] {
@ -1080,6 +1093,27 @@ enum JavaContentScanner {
return folders return folders
} }
nonisolated private static func contentScanRoots(for sourceRootURL: URL, fileManager: FileManager) -> [URL] {
let standardizedRoot = sourceRootURL.standardizedFileURL
if javaProbeScore(for: standardizedRoot, fileManager: fileManager).value > 0 {
return [standardizedRoot]
}
let discoveredRoots = boundedCandidateFolders(
from: standardizedRoot,
maxDepth: 4,
maxFolderCount: 600,
fileManager: fileManager
).filter { candidateURL in
candidateURL != standardizedRoot
&& javaProbeScore(for: candidateURL, fileManager: fileManager).value > 0
}
return uniqueStandardizedURLs(discoveredRoots).sorted {
$0.path.localizedStandardCompare($1.path) == .orderedAscending
}
}
nonisolated private static func uniqueStandardizedURLs(_ urls: [URL]) -> [URL] { nonisolated private static func uniqueStandardizedURLs(_ urls: [URL]) -> [URL] {
var seen = Set<String>() var seen = Set<String>()
var result: [URL] = [] var result: [URL] = []
@ -1099,6 +1133,7 @@ enum JavaContentScanner {
nonisolated private static func collectionSnapshot( nonisolated private static func collectionSnapshot(
for collectionURL: URL, for collectionURL: URL,
sourceRootURL: URL,
fileManager: FileManager fileManager: FileManager
) -> CollectionSnapshot? { ) -> CollectionSnapshot? {
guard fileManager.fileExists(atPath: collectionURL.path) else { guard fileManager.fileExists(atPath: collectionURL.path) else {
@ -1135,12 +1170,15 @@ enum JavaContentScanner {
].joined(separator: "@") ].joined(separator: "@")
}.joined(separator: "|") }.joined(separator: "|")
let folderName = relativePath(from: sourceRootURL.standardizedFileURL, to: collectionURL.standardizedFileURL)
?? collectionURL.lastPathComponent
return CollectionSnapshot( return CollectionSnapshot(
folderName: collectionURL.lastPathComponent, folderName: folderName,
modifiedDate: modifiedDate, modifiedDate: modifiedDate,
childDirectoryCount: childSnapshots.count, childDirectoryCount: childSnapshots.count,
fingerprint: [ fingerprint: [
collectionURL.lastPathComponent, folderName,
String(childSnapshots.count), String(childSnapshots.count),
modifiedDate?.timeIntervalSince1970.formatted() ?? "nil", modifiedDate?.timeIntervalSince1970.formatted() ?? "nil",
childFingerprint childFingerprint
@ -1148,6 +1186,16 @@ enum JavaContentScanner {
) )
} }
nonisolated private static func relativePath(from rootURL: URL, to childURL: URL) -> String? {
let rootPath = rootURL.standardizedFileURL.path
let childPath = childURL.standardizedFileURL.path
guard childPath.hasPrefix(rootPath + "/") else {
return nil
}
return String(childPath.dropFirst(rootPath.count + 1))
}
nonisolated private static func displayName(for item: MinecraftContentItem) -> String { nonisolated private static func displayName(for item: MinecraftContentItem) -> String {
guard item.contentKind == .world else { guard item.contentKind == .world else {
return item.folderName return item.folderName

View File

@ -385,6 +385,34 @@ struct World_Manager_for_MinecraftTests {
#expect(candidates.first?.detectedKinds.contains(.mod) == true) #expect(candidates.first?.detectedKinds.contains(.mod) == true)
} }
@Test func javaAggregateRootDiscoversNestedInstanceItems() async throws {
let fileManager = FileManager.default
let workingURL = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
let firstInstanceURL = workingURL.appendingPathComponent("a/b/c", isDirectory: true)
let secondInstanceURL = workingURL.appendingPathComponent("a/e/f", isDirectory: true)
defer { try? fileManager.removeItem(at: workingURL) }
try fileManager.createDirectory(
at: firstInstanceURL.appendingPathComponent("mods", isDirectory: true),
withIntermediateDirectories: true
)
try Data("jar".utf8).write(to: firstInstanceURL.appendingPathComponent("mods/ExampleMod.jar"))
try fileManager.createDirectory(
at: secondInstanceURL.appendingPathComponent("resourcepacks", isDirectory: true),
withIntermediateDirectories: true
)
try Data("zip".utf8).write(to: secondInstanceURL.appendingPathComponent("resourcepacks/ExamplePack.zip"))
let items = try JavaContentScanner.discoverItems(in: workingURL)
let snapshots = JavaContentScanner.collectionSnapshots(in: workingURL)
#expect(items.contains { $0.contentKind == .mod && $0.folderName == "ExampleMod.jar" })
#expect(items.contains { $0.contentKind == .resourcePack && $0.folderName == "ExamplePack.zip" })
#expect(snapshots.map(\.folderName).contains("a/b/c/mods"))
#expect(snapshots.map(\.folderName).contains("a/e/f/resourcepacks"))
}
@Test func sourceLibraryAddSourceResolvesJavaWrapperFolder() async throws { @Test func sourceLibraryAddSourceResolvesJavaWrapperFolder() async throws {
let fileManager = FileManager.default let fileManager = FileManager.default
let rootURL = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) let rootURL = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)