Extract source scan policy helpers
This commit is contained in:
parent
c7659ccda3
commit
1e0447a2b1
@ -269,13 +269,13 @@ final class SourceLibrary: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
let previousSource = source
|
let previousSource = source
|
||||||
let performanceContext = performanceContext(for: source)
|
let performanceContext = SourceScanPolicy.performanceContext(for: source)
|
||||||
|
|
||||||
updateSource(sourceID) { source in
|
updateSource(sourceID) { source in
|
||||||
source.isScanning = true
|
source.isScanning = true
|
||||||
source.scanError = nil
|
source.scanError = nil
|
||||||
source.scanDiagnostic = nil
|
source.scanDiagnostic = nil
|
||||||
source.scanStatus = initialScanStatus(for: source, mode: mode)
|
source.scanStatus = SourceScanPolicy.initialStatus(for: source, mode: mode)
|
||||||
source.scanProgress = nil
|
source.scanProgress = nil
|
||||||
source.indexedItemCount = 0
|
source.indexedItemCount = 0
|
||||||
source.indexedDetailCount = 0
|
source.indexedDetailCount = 0
|
||||||
@ -301,7 +301,7 @@ final class SourceLibrary: ObservableObject {
|
|||||||
|
|
||||||
updateSource(sourceID) { source in
|
updateSource(sourceID) { source in
|
||||||
source.availability = .available
|
source.availability = .available
|
||||||
source.scanStatus = scanningLibraryStatus(for: source, mode: mode)
|
source.scanStatus = SourceScanPolicy.scanningLibraryStatus(for: source, mode: mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
@ -367,7 +367,7 @@ final class SourceLibrary: ObservableObject {
|
|||||||
let itemForIndex: MinecraftContentItem
|
let itemForIndex: MinecraftContentItem
|
||||||
if shouldReconcileFromCache,
|
if shouldReconcileFromCache,
|
||||||
let cachedItem = previousItemsByID[item.id],
|
let cachedItem = previousItemsByID[item.id],
|
||||||
shouldReuseCachedItem(
|
SourceScanPolicy.shouldReuseCachedItem(
|
||||||
cachedItem,
|
cachedItem,
|
||||||
forDiscoveredItem: item,
|
forDiscoveredItem: item,
|
||||||
source: source,
|
source: source,
|
||||||
@ -500,7 +500,7 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}
|
}
|
||||||
updateSource(sourceID) { source in
|
updateSource(sourceID) { source in
|
||||||
if source.origin.kind == .localFolder {
|
if source.origin.kind == .localFolder {
|
||||||
source.snapshot = buildSnapshot(for: source, scanRootURL: scanContextURL, packMetadataByItemID: [:])
|
source.snapshot = SourceScanPolicy.buildSnapshot(for: source, scanRootURL: scanContextURL)
|
||||||
} else {
|
} else {
|
||||||
source.snapshot = nil
|
source.snapshot = nil
|
||||||
}
|
}
|
||||||
@ -572,7 +572,7 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}
|
}
|
||||||
updateSource(sourceID) { source in
|
updateSource(sourceID) { source in
|
||||||
if source.origin.kind == .localFolder {
|
if source.origin.kind == .localFolder {
|
||||||
source.snapshot = buildSnapshot(for: source, scanRootURL: scanContextURL, packMetadataByItemID: [:])
|
source.snapshot = SourceScanPolicy.buildSnapshot(for: source, scanRootURL: scanContextURL)
|
||||||
} else {
|
} else {
|
||||||
source.snapshot = nil
|
source.snapshot = nil
|
||||||
}
|
}
|
||||||
@ -593,7 +593,7 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
updateSource(sourceID) { source in
|
updateSource(sourceID) { source in
|
||||||
if shouldPreservePartialScanContent(currentSource: source, previousSource: previousSource) {
|
if SourceScanRecovery.shouldPreservePartialResults(currentSource: source, previousSource: previousSource) {
|
||||||
source.scanStatus = source.indexedItemCount == 0
|
source.scanStatus = source.indexedItemCount == 0
|
||||||
? previousSource.scanStatus
|
? previousSource.scanStatus
|
||||||
: "Loaded \(source.indexedDetailCount) items."
|
: "Loaded \(source.indexedDetailCount) items."
|
||||||
@ -601,21 +601,17 @@ final class SourceLibrary: ObservableObject {
|
|||||||
? "Showing the most recent partial scan results."
|
? "Showing the most recent partial scan results."
|
||||||
: "Showing the most recent partial scan results after the scan stopped early."
|
: "Showing the most recent partial scan results after the scan stopped early."
|
||||||
if source.origin.kind == .localFolder, !source.rawItems.isEmpty {
|
if source.origin.kind == .localFolder, !source.rawItems.isEmpty {
|
||||||
source.snapshot = buildSnapshot(
|
source.snapshot = SourceScanPolicy.buildSnapshot(for: source, scanRootURL: scanContextURL)
|
||||||
for: source,
|
|
||||||
scanRootURL: scanContextURL,
|
|
||||||
packMetadataByItemID: [:]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
restoreScannedContent(from: previousSource, into: &source)
|
SourceScanRecovery.restoreIndexedState(from: previousSource, into: &source)
|
||||||
}
|
}
|
||||||
source.availability = Task.isCancelled
|
source.availability = Task.isCancelled
|
||||||
? previousSource.availability
|
? previousSource.availability
|
||||||
: availabilityStatus(for: error, defaultingTo: previousSource.availability)
|
: SourceScanPolicy.availabilityStatus(for: error, defaultingTo: previousSource.availability)
|
||||||
source.scanError = Task.isCancelled
|
source.scanError = Task.isCancelled
|
||||||
? previousSource.scanError
|
? previousSource.scanError
|
||||||
: friendlyScanError(for: error, source: source)
|
: SourceScanPolicy.friendlyError(for: error, source: source)
|
||||||
source.scanDiagnostic = Task.isCancelled
|
source.scanDiagnostic = Task.isCancelled
|
||||||
? source.scanDiagnostic
|
? source.scanDiagnostic
|
||||||
: (source.scanDiagnostic ?? error.localizedDescription)
|
: (source.scanDiagnostic ?? error.localizedDescription)
|
||||||
@ -779,55 +775,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func restoreScannedContent(from previousSource: MinecraftSource, into source: inout MinecraftSource) {
|
|
||||||
source.displayItems = previousSource.displayItems
|
|
||||||
source.rawItems = previousSource.rawItems
|
|
||||||
source.logicalPacks = previousSource.logicalPacks
|
|
||||||
source.logicalWorlds = previousSource.logicalWorlds
|
|
||||||
source.packInstances = previousSource.packInstances
|
|
||||||
source.worldPackRelationships = previousSource.worldPackRelationships
|
|
||||||
source.snapshot = previousSource.snapshot
|
|
||||||
source.indexedItemCount = previousSource.indexedItemCount
|
|
||||||
source.indexedDetailCount = previousSource.indexedDetailCount
|
|
||||||
source.previewLoadedCount = previousSource.previewLoadedCount
|
|
||||||
source.sizeLoadedCount = previousSource.sizeLoadedCount
|
|
||||||
source.scanProgress = previousSource.scanProgress
|
|
||||||
source.lastScanDate = previousSource.lastScanDate
|
|
||||||
}
|
|
||||||
|
|
||||||
private func shouldPreservePartialScanContent(
|
|
||||||
currentSource: MinecraftSource,
|
|
||||||
previousSource: MinecraftSource
|
|
||||||
) -> Bool {
|
|
||||||
if currentSource.rawItems.count > previousSource.rawItems.count {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if currentSource.indexedDetailCount > previousSource.indexedDetailCount {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if currentSource.previewLoadedCount > previousSource.previewLoadedCount {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if currentSource.sizeLoadedCount > previousSource.sizeLoadedCount {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private func performanceContext(for source: MinecraftSource) -> String {
|
|
||||||
switch source.origin {
|
|
||||||
case .localFolder:
|
|
||||||
return "source=\(source.displayName) kind=local"
|
|
||||||
case .connectedDevice(let device, let container):
|
|
||||||
let transport = device.connection == .usb ? "usb" : "network"
|
|
||||||
return "source=\(source.displayName) kind=connected-device transport=\(transport) udid=\(device.udid) app=\(container.appID)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func logScanStage(
|
private func logScanStage(
|
||||||
_ stage: String,
|
_ stage: String,
|
||||||
elapsed: TimeInterval,
|
elapsed: TimeInterval,
|
||||||
@ -853,33 +800,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func friendlyScanError(for error: Error, source: MinecraftSource) -> String {
|
|
||||||
let description = error.localizedDescription
|
|
||||||
|
|
||||||
guard source.origin.kind == .connectedDevice else {
|
|
||||||
return "Failed to scan folder: \(description)"
|
|
||||||
}
|
|
||||||
|
|
||||||
if description.contains("AMDeviceCreateHouseArrestService returned -402653093")
|
|
||||||
|| description.contains("kAMDServiceLimitError") {
|
|
||||||
return "Device is busy. Too many device access sessions were open, so the scan could not start."
|
|
||||||
}
|
|
||||||
|
|
||||||
if description.localizedCaseInsensitiveContains("InstallationLookupFailed") {
|
|
||||||
return "The device refused access to the Minecraft app container."
|
|
||||||
}
|
|
||||||
|
|
||||||
if description.localizedCaseInsensitiveContains("not paired") {
|
|
||||||
return "The device is not paired with this Mac."
|
|
||||||
}
|
|
||||||
|
|
||||||
if description.localizedCaseInsensitiveContains("no longer available") {
|
|
||||||
return "The device disconnected during the scan."
|
|
||||||
}
|
|
||||||
|
|
||||||
return "Failed to scan device library."
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateSource(_ sourceID: URL, mutate: (inout MinecraftSource) -> Void) {
|
private func updateSource(_ sourceID: URL, mutate: (inout MinecraftSource) -> Void) {
|
||||||
guard let index = sources.firstIndex(where: { $0.id == sourceID }) else {
|
guard let index = sources.firstIndex(where: { $0.id == sourceID }) else {
|
||||||
return
|
return
|
||||||
@ -1780,116 +1700,6 @@ final class SourceLibrary: ObservableObject {
|
|||||||
return source.previewLoadedCount < itemCount || source.sizeLoadedCount < itemCount
|
return source.previewLoadedCount < itemCount || source.sizeLoadedCount < itemCount
|
||||||
}
|
}
|
||||||
|
|
||||||
private func initialScanStatus(for source: MinecraftSource, mode: SourceDiscoveryMode) -> String {
|
|
||||||
switch (source.origin, mode) {
|
|
||||||
case (.localFolder, .fullScan):
|
|
||||||
return "Preparing folder scan..."
|
|
||||||
case (.localFolder, .reconcile):
|
|
||||||
return "Preparing cached library refresh..."
|
|
||||||
case (.connectedDevice, .fullScan):
|
|
||||||
return "Connecting to device and discovering Minecraft items..."
|
|
||||||
case (.connectedDevice, .reconcile):
|
|
||||||
return "Connecting to device and refreshing cached library..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func scanningLibraryStatus(for source: MinecraftSource, mode: SourceDiscoveryMode) -> String {
|
|
||||||
switch (source.origin, mode) {
|
|
||||||
case (.localFolder, .fullScan):
|
|
||||||
return "Scanning Minecraft library..."
|
|
||||||
case (.localFolder, .reconcile):
|
|
||||||
return "Reconciling cached library..."
|
|
||||||
case (.connectedDevice, .fullScan):
|
|
||||||
return "Scanning Minecraft library on device..."
|
|
||||||
case (.connectedDevice, .reconcile):
|
|
||||||
return "Reconciling cached device library..."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func shouldReuseCachedItem(
|
|
||||||
_ cachedItem: MinecraftContentItem,
|
|
||||||
forDiscoveredItem discoveredItem: MinecraftContentItem,
|
|
||||||
source: MinecraftSource,
|
|
||||||
previousSnapshot: ItemSnapshot?
|
|
||||||
) -> Bool {
|
|
||||||
guard cachedItem.contentType == discoveredItem.contentType else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
guard cachedItem.metadataLoaded, cachedItem.previewLoaded, cachedItem.sizeLoaded else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch source.origin.kind {
|
|
||||||
case .localFolder:
|
|
||||||
guard let previousSnapshot else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentModifiedDate = try? discoveredItem.folderURL
|
|
||||||
.resourceValues(forKeys: [.contentModificationDateKey])
|
|
||||||
.contentModificationDate
|
|
||||||
return previousSnapshot.modifiedDate == currentModifiedDate
|
|
||||||
case .connectedDevice:
|
|
||||||
return cachedItem.folderName == discoveredItem.folderName
|
|
||||||
&& cachedItem.displayName == discoveredItem.displayName
|
|
||||||
&& cachedItem.hasKnownIcon == discoveredItem.hasKnownIcon
|
|
||||||
&& cachedItem.packUUID == discoveredItem.packUUID
|
|
||||||
&& cachedItem.packVersion == discoveredItem.packVersion
|
|
||||||
&& cachedItem.packMetadataDetails == discoveredItem.packMetadataDetails
|
|
||||||
&& cachedItem.packReferences == discoveredItem.packReferences
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func buildSnapshot(
|
|
||||||
for source: MinecraftSource,
|
|
||||||
scanRootURL: URL,
|
|
||||||
packMetadataByItemID: [URL: PackMetadata]
|
|
||||||
) -> SourceSnapshot {
|
|
||||||
let collectionSnapshots = WorldScanner.collectionSnapshots(in: scanRootURL)
|
|
||||||
|
|
||||||
let itemSnapshots = source.rawItems.map { item in
|
|
||||||
let relativePath = item.folderURL.path.replacingOccurrences(of: scanRootURL.path + "/", with: "")
|
|
||||||
let metadata = packMetadataByItemID[item.id]
|
|
||||||
return ItemSnapshot(
|
|
||||||
id: item.id,
|
|
||||||
relativePath: relativePath,
|
|
||||||
modifiedDate: item.modifiedDate,
|
|
||||||
sizeBytes: item.sizeBytes,
|
|
||||||
packUUID: metadata?.uuid,
|
|
||||||
packVersion: metadata?.version
|
|
||||||
)
|
|
||||||
}.sorted { (lhs: ItemSnapshot, rhs: ItemSnapshot) in
|
|
||||||
lhs.relativePath.localizedStandardCompare(rhs.relativePath) == .orderedAscending
|
|
||||||
}
|
|
||||||
|
|
||||||
let rootModifiedDate = try? scanRootURL
|
|
||||||
.resourceValues(forKeys: [.contentModificationDateKey])
|
|
||||||
.contentModificationDate
|
|
||||||
|
|
||||||
return SourceSnapshot(
|
|
||||||
sourceID: source.id,
|
|
||||||
rootModifiedDate: rootModifiedDate,
|
|
||||||
collectionSnapshots: collectionSnapshots,
|
|
||||||
itemSnapshots: itemSnapshots
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func availabilityStatus(for error: Error, defaultingTo currentAvailability: SourceAvailability) -> SourceAvailability {
|
|
||||||
if let accessError = error as? SourceAccessError {
|
|
||||||
switch accessError {
|
|
||||||
case .deviceUnavailable:
|
|
||||||
return .disconnected
|
|
||||||
case .deviceNotTrusted:
|
|
||||||
return .limited
|
|
||||||
case .appNotAccessible, .minecraftFolderMissing, .accessFailed:
|
|
||||||
return .unavailable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentAvailability
|
|
||||||
}
|
|
||||||
|
|
||||||
private func shouldPreferPackItem(_ candidate: MinecraftContentItem, over existing: MinecraftContentItem) -> Bool {
|
private func shouldPreferPackItem(_ candidate: MinecraftContentItem, over existing: MinecraftContentItem) -> Bool {
|
||||||
let candidateEmbedded = isEmbeddedWorldPack(candidate)
|
let candidateEmbedded = isEmbeddedWorldPack(candidate)
|
||||||
let existingEmbedded = isEmbeddedWorldPack(existing)
|
let existingEmbedded = isEmbeddedWorldPack(existing)
|
||||||
|
|||||||
195
World Manager for Minecraft/Services/SourceScanning.swift
Normal file
195
World Manager for Minecraft/Services/SourceScanning.swift
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
//
|
||||||
|
// SourceScanning.swift
|
||||||
|
// World Manager for Minecraft
|
||||||
|
//
|
||||||
|
// Created by OpenAI on 2026-05-28.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum SourceScanPolicy {
|
||||||
|
static func initialStatus(for source: MinecraftSource, mode: SourceDiscoveryMode) -> String {
|
||||||
|
switch (source.origin, mode) {
|
||||||
|
case (.localFolder, .fullScan):
|
||||||
|
return "Preparing folder scan..."
|
||||||
|
case (.localFolder, .reconcile):
|
||||||
|
return "Preparing cached library refresh..."
|
||||||
|
case (.connectedDevice, .fullScan):
|
||||||
|
return "Connecting to device and discovering Minecraft items..."
|
||||||
|
case (.connectedDevice, .reconcile):
|
||||||
|
return "Connecting to device and refreshing cached library..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func scanningLibraryStatus(for source: MinecraftSource, mode: SourceDiscoveryMode) -> String {
|
||||||
|
switch (source.origin, mode) {
|
||||||
|
case (.localFolder, .fullScan):
|
||||||
|
return "Scanning Minecraft library..."
|
||||||
|
case (.localFolder, .reconcile):
|
||||||
|
return "Reconciling cached library..."
|
||||||
|
case (.connectedDevice, .fullScan):
|
||||||
|
return "Scanning Minecraft library on device..."
|
||||||
|
case (.connectedDevice, .reconcile):
|
||||||
|
return "Reconciling cached device library..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func performanceContext(for source: MinecraftSource) -> String {
|
||||||
|
switch source.origin {
|
||||||
|
case .localFolder:
|
||||||
|
return "source=\(source.displayName) kind=local"
|
||||||
|
case .connectedDevice(let device, let container):
|
||||||
|
let transport = device.connection == .usb ? "usb" : "network"
|
||||||
|
return "source=\(source.displayName) kind=connected-device transport=\(transport) udid=\(device.udid) app=\(container.appID)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func friendlyError(for error: Error, source: MinecraftSource) -> String {
|
||||||
|
let description = error.localizedDescription
|
||||||
|
|
||||||
|
guard source.origin.kind == .connectedDevice else {
|
||||||
|
return "Failed to scan folder: \(description)"
|
||||||
|
}
|
||||||
|
|
||||||
|
if description.contains("AMDeviceCreateHouseArrestService returned -402653093")
|
||||||
|
|| description.contains("kAMDServiceLimitError") {
|
||||||
|
return "Device is busy. Too many device access sessions were open, so the scan could not start."
|
||||||
|
}
|
||||||
|
|
||||||
|
if description.localizedCaseInsensitiveContains("InstallationLookupFailed") {
|
||||||
|
return "The device refused access to the Minecraft app container."
|
||||||
|
}
|
||||||
|
|
||||||
|
if description.localizedCaseInsensitiveContains("not paired") {
|
||||||
|
return "The device is not paired with this Mac."
|
||||||
|
}
|
||||||
|
|
||||||
|
if description.localizedCaseInsensitiveContains("no longer available") {
|
||||||
|
return "The device disconnected during the scan."
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Failed to scan device library."
|
||||||
|
}
|
||||||
|
|
||||||
|
static func availabilityStatus(
|
||||||
|
for error: Error,
|
||||||
|
defaultingTo currentAvailability: SourceAvailability
|
||||||
|
) -> SourceAvailability {
|
||||||
|
if let accessError = error as? SourceAccessError {
|
||||||
|
switch accessError {
|
||||||
|
case .deviceUnavailable:
|
||||||
|
return .disconnected
|
||||||
|
case .deviceNotTrusted:
|
||||||
|
return .limited
|
||||||
|
case .appNotAccessible, .minecraftFolderMissing, .accessFailed:
|
||||||
|
return .unavailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentAvailability
|
||||||
|
}
|
||||||
|
|
||||||
|
static func shouldReuseCachedItem(
|
||||||
|
_ cachedItem: MinecraftContentItem,
|
||||||
|
forDiscoveredItem discoveredItem: MinecraftContentItem,
|
||||||
|
source: MinecraftSource,
|
||||||
|
previousSnapshot: ItemSnapshot?
|
||||||
|
) -> Bool {
|
||||||
|
guard cachedItem.contentType == discoveredItem.contentType else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
guard cachedItem.metadataLoaded, cachedItem.previewLoaded, cachedItem.sizeLoaded else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch source.origin.kind {
|
||||||
|
case .localFolder:
|
||||||
|
guard let previousSnapshot else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentModifiedDate = try? discoveredItem.folderURL
|
||||||
|
.resourceValues(forKeys: [.contentModificationDateKey])
|
||||||
|
.contentModificationDate
|
||||||
|
return previousSnapshot.modifiedDate == currentModifiedDate
|
||||||
|
case .connectedDevice:
|
||||||
|
return cachedItem.folderName == discoveredItem.folderName
|
||||||
|
&& cachedItem.displayName == discoveredItem.displayName
|
||||||
|
&& cachedItem.hasKnownIcon == discoveredItem.hasKnownIcon
|
||||||
|
&& cachedItem.packUUID == discoveredItem.packUUID
|
||||||
|
&& cachedItem.packVersion == discoveredItem.packVersion
|
||||||
|
&& cachedItem.packMetadataDetails == discoveredItem.packMetadataDetails
|
||||||
|
&& cachedItem.packReferences == discoveredItem.packReferences
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func buildSnapshot(for source: MinecraftSource, scanRootURL: URL) -> SourceSnapshot {
|
||||||
|
let collectionSnapshots = WorldScanner.collectionSnapshots(in: scanRootURL)
|
||||||
|
|
||||||
|
let itemSnapshots = source.rawItems.map { item in
|
||||||
|
ItemSnapshot(
|
||||||
|
id: item.id,
|
||||||
|
relativePath: item.folderURL.path.replacingOccurrences(of: scanRootURL.path + "/", with: ""),
|
||||||
|
modifiedDate: item.modifiedDate,
|
||||||
|
sizeBytes: item.sizeBytes,
|
||||||
|
packUUID: nil,
|
||||||
|
packVersion: nil
|
||||||
|
)
|
||||||
|
}.sorted { lhs, rhs in
|
||||||
|
lhs.relativePath.localizedStandardCompare(rhs.relativePath) == .orderedAscending
|
||||||
|
}
|
||||||
|
|
||||||
|
let rootModifiedDate = try? scanRootURL
|
||||||
|
.resourceValues(forKeys: [.contentModificationDateKey])
|
||||||
|
.contentModificationDate
|
||||||
|
|
||||||
|
return SourceSnapshot(
|
||||||
|
sourceID: source.id,
|
||||||
|
rootModifiedDate: rootModifiedDate,
|
||||||
|
collectionSnapshots: collectionSnapshots,
|
||||||
|
itemSnapshots: itemSnapshots
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SourceScanRecovery {
|
||||||
|
static func restoreIndexedState(from previousSource: MinecraftSource, into source: inout MinecraftSource) {
|
||||||
|
source.displayItems = previousSource.displayItems
|
||||||
|
source.rawItems = previousSource.rawItems
|
||||||
|
source.logicalPacks = previousSource.logicalPacks
|
||||||
|
source.logicalWorlds = previousSource.logicalWorlds
|
||||||
|
source.packInstances = previousSource.packInstances
|
||||||
|
source.worldPackRelationships = previousSource.worldPackRelationships
|
||||||
|
source.snapshot = previousSource.snapshot
|
||||||
|
source.indexedItemCount = previousSource.indexedItemCount
|
||||||
|
source.indexedDetailCount = previousSource.indexedDetailCount
|
||||||
|
source.previewLoadedCount = previousSource.previewLoadedCount
|
||||||
|
source.sizeLoadedCount = previousSource.sizeLoadedCount
|
||||||
|
source.scanProgress = previousSource.scanProgress
|
||||||
|
source.lastScanDate = previousSource.lastScanDate
|
||||||
|
}
|
||||||
|
|
||||||
|
static func shouldPreservePartialResults(
|
||||||
|
currentSource: MinecraftSource,
|
||||||
|
previousSource: MinecraftSource
|
||||||
|
) -> Bool {
|
||||||
|
if currentSource.rawItems.count > previousSource.rawItems.count {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentSource.indexedDetailCount > previousSource.indexedDetailCount {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentSource.previewLoadedCount > previousSource.previewLoadedCount {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentSource.sizeLoadedCount > previousSource.sizeLoadedCount {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user