Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
bca1672
Add method to begin implementation of attempt invalid parent item ide…
claucambra May 23, 2025
c19def3
Make concurrent chunked array processing methods throwing by default
claucambra May 23, 2025
5699556
Add convenience method to easily generate errors for files database m…
claucambra May 25, 2025
8b02038
Add errors for parent metadata not found
claucambra May 25, 2025
1a94def
Throw error in sendable item metadata conversion when parent item met…
claucambra May 25, 2025
3addf78
Adapt to db error changes
claucambra May 25, 2025
bebc9f5
Attempt handling of invalid parent item identifier when enumerating
claucambra May 25, 2025
ea479b8
Remove unnecessary double-fetch of parent directory metadata
claucambra May 25, 2025
43b17dc
Add fallback method in db manager that remotely retrieves parent item…
claucambra May 25, 2025
d124c84
Add test for db manager parent item identifier procedure with fallback
claucambra May 26, 2025
6f08fe3
Improve error handling and logging around invalid parent recovery in …
claucambra May 26, 2025
502f3c0
Add enumerate file test with missing parent in database
claucambra May 26, 2025
4bc37a6
Some more comments and logging in enumerator invalid parent handling
claucambra May 26, 2025
b7c9356
Use fallback parent item identifier method during trashing
claucambra May 26, 2025
7bede93
Use parent item identifier with fallback method during item fetch
claucambra May 26, 2025
6911bd2
Make stored item retrieval async to leverage fallback parent item ide…
claucambra May 26, 2025
63eb9a4
Rectify logging in sendable item metadata array conversion
claucambra May 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,25 @@
public let databaseFilename = "fileproviderextdatabase.realm"

public final class FilesDatabaseManager: Sendable {
enum ErrorCode: Int {
public enum ErrorCode: Int {
case metadataNotFound = -1000
case parentMetadataNotFound = -1001
}
public enum ErrorUserInfoKey: String {
case missingParentServerUrlAndFileName = "MissingParentServerUrlAndFileName"
}
static let errorDomain = "FilesDatabaseManager"
static func error(code: ErrorCode, userInfo: [String: String]) -> NSError {
NSError(domain: Self.errorDomain, code: code.rawValue, userInfo: userInfo)
}
static func parentMetadataNotFoundError(itemUrl: String) -> NSError {
error(
code: .parentMetadataNotFound,
userInfo: [ErrorUserInfoKey.missingParentServerUrlAndFileName.rawValue: itemUrl]
)
}

private static let schemaVersion = stable2_0SchemaVersion
static let errorDomain = "FilesDatabaseManager"
static let logger = Logger(subsystem: Logger.subsystem, category: "filesdatabase")
let account: Account

Expand Down Expand Up @@ -553,34 +566,50 @@
return .trashContainer
}

guard let itemParentDirectory = parentDirectoryMetadataForItem(metadata) else {
guard let parentDirectoryMetadata = parentDirectoryMetadataForItem(metadata) else {
Self.logger.error(
"""
Could not get item parent directory metadata for metadata.
Could not get item parent directory item metadata for metadata.
ocID: \(metadata.ocId, privacy: .public),
etag: \(metadata.etag, privacy: .public),
etag: \(metadata.etag, privacy: .public),
fileName: \(metadata.fileName, privacy: .public),
serverUrl: \(metadata.serverUrl, privacy: .public),
account: \(metadata.account, privacy: .public),
"""
)
return nil
}
return NSFileProviderItemIdentifier(parentDirectoryMetadata.ocId)
}

if let parentDirectoryMetadata = itemMetadata(ocId: itemParentDirectory.ocId) {
return NSFileProviderItemIdentifier(parentDirectoryMetadata.ocId)
public func parentItemIdentifierWithRemoteFallback(
fromMetadata metadata: SendableItemMetadata,
remoteInterface: RemoteInterface,
account: Account
) async -> NSFileProviderItemIdentifier? {
if let parentItemIdentifier = parentItemIdentifierFromMetadata(metadata) {
return parentItemIdentifier
}

Self.logger.error(
"""
Could not get item parent directory item metadata for metadata.
ocID: \(metadata.ocId, privacy: .public),
etag: \(metadata.etag, privacy: .public),
fileName: \(metadata.fileName, privacy: .public),
serverUrl: \(metadata.serverUrl, privacy: .public),
account: \(metadata.account, privacy: .public),
"""
let (metadatas, _, _, _, error) = await Enumerator.readServerUrl(
metadata.serverUrl,
account: account,
remoteInterface: remoteInterface,
dbManager: self,
depth: .target
)
return nil
guard error == nil, let parentMetadata = metadatas?.first else {
Self.logger.error(
"""
Could not retrieve parent item identifier remotely, received error.
target metadata: \(metadata.ocId, privacy: .public)
target filename: \(metadata.fileName, privacy: .public)
received metadatas: \(metadatas?.count ?? 0, privacy: .public)
error: \(error?.errorDescription ?? "NO ERROR", privacy: .public)
"""
)
return nil

Check warning on line 611 in Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift

View check run for this annotation

Codecov / codecov/patch

Sources/NextcloudFileProviderKit/Database/FilesDatabaseManager.swift#L602-L611

Added lines #L602 - L611 were not covered by tests
}
return NSFileProviderItemIdentifier(parentMetadata.ocId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,18 @@
}

Task { [metadatas] in
let items = await metadatas.toFileProviderItems(
account: account, remoteInterface: remoteInterface, dbManager: dbManager
)

Task { @MainActor in
observer.didEnumerate(items)
Self.logger.info("Did enumerate \(items.count) trash items")
observer.finishEnumerating(upTo: fileProviderPageforNumPage(numPage))
do {
let items = try await metadatas.toFileProviderItems(
account: account, remoteInterface: remoteInterface, dbManager: dbManager
)
Task { @MainActor in
observer.didEnumerate(items)
Self.logger.info("Did enumerate \(items.count) trash items")
observer.finishEnumerating(upTo: fileProviderPageforNumPage(numPage))
}
} catch let error {
Self.logger.info("Unexpected error enumerating trash items, observing error.")
Task { @MainActor in observer.finishEnumeratingWithError(error) }

Check warning on line 39 in Sources/NextcloudFileProviderKit/Enumeration/Enumerator+Trash.swift

View check run for this annotation

Codecov / codecov/patch

Sources/NextcloudFileProviderKit/Enumeration/Enumerator+Trash.swift#L38-L39

Added lines #L38 - L39 were not covered by tests
}
}
}
Expand Down
163 changes: 132 additions & 31 deletions Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -612,28 +612,56 @@
remoteInterface: RemoteInterface,
dbManager: FilesDatabaseManager,
numPage: Int,
itemMetadatas: [SendableItemMetadata]
itemMetadatas: [SendableItemMetadata],
handleInvalidParent: Bool = true
) {
Task {
let items = await itemMetadatas.toFileProviderItems(
account: account, remoteInterface: remoteInterface, dbManager: dbManager
)
do {
let items = try await itemMetadatas.toFileProviderItems(
account: account, remoteInterface: remoteInterface, dbManager: dbManager
)

Task { @MainActor in
observer.didEnumerate(items)
Self.logger.info("Did enumerate \(items.count) items")

// TODO: Handle paging properly
/*
if items.count == maxItemsPerFileProviderPage {
let nextPage = numPage + 1
let providerPage = NSFileProviderPage("\(nextPage)".data(using: .utf8)!)
observer.finishEnumerating(upTo: providerPage)
} else {
observer.finishEnumerating(upTo: nil)
}
*/
observer.finishEnumerating(upTo: fileProviderPageforNumPage(numPage))
Task { @MainActor in
observer.didEnumerate(items)
Self.logger.info("Did enumerate \(items.count) items")

// TODO: Handle paging properly
/*
if items.count == maxItemsPerFileProviderPage {
let nextPage = numPage + 1
let providerPage = NSFileProviderPage("\(nextPage)".data(using: .utf8)!)
observer.finishEnumerating(upTo: providerPage)
} else {
observer.finishEnumerating(upTo: nil)
}
*/
observer.finishEnumerating(upTo: fileProviderPageforNumPage(numPage))
}
} catch let error as NSError { // This error can only mean a missing parent item identifier
guard handleInvalidParent else {
Self.logger.info("Not handling invalid parent in enumeration")
observer.finishEnumeratingWithError(error)
return
}
do {
let metadata = try await Self.attemptInvalidParentRecovery(
error: error,
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager
)
Self.completeEnumerationObserver(
observer,
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
numPage: numPage,
itemMetadatas: [metadata] + itemMetadatas,
handleInvalidParent: false
)
} catch let error {
observer.finishEnumeratingWithError(error)
}

Check warning on line 664 in Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift

View check run for this annotation

Codecov / codecov/patch

Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift#L641-L664

Added lines #L641 - L664 were not covered by tests
}
}
}
Expand All @@ -647,7 +675,8 @@
dbManager: FilesDatabaseManager,
newMetadatas: [SendableItemMetadata]?,
updatedMetadatas: [SendableItemMetadata]?,
deletedMetadatas: [SendableItemMetadata]?
deletedMetadatas: [SendableItemMetadata]?,
handleInvalidParent: Bool = true
) {
guard newMetadatas != nil || updatedMetadatas != nil || deletedMetadatas != nil else {
Self.logger.error(
Expand Down Expand Up @@ -687,23 +716,95 @@
}

Task { [allUpdatedMetadatas, allDeletedMetadatas] in
let updatedItems = await allUpdatedMetadatas.toFileProviderItems(
account: account, remoteInterface: remoteInterface, dbManager: dbManager
)
do {
let updatedItems = try await allUpdatedMetadatas.toFileProviderItems(
account: account, remoteInterface: remoteInterface, dbManager: dbManager
)

Task { @MainActor in
if !updatedItems.isEmpty {
observer.didUpdate(updatedItems)
}
Task { @MainActor in
if !updatedItems.isEmpty {
observer.didUpdate(updatedItems)
}

Self.logger.info(
Self.logger.info(
"""
Processed \(updatedItems.count) new or updated metadatas.
\(allDeletedMetadatas.count) deleted metadatas.
\(allDeletedMetadatas.count) deleted metadatas.
"""
)
observer.finishEnumeratingChanges(upTo: anchor, moreComing: false)
)
observer.finishEnumeratingChanges(upTo: anchor, moreComing: false)
}
} catch let error as NSError { // This error can only mean a missing parent item identifier
guard handleInvalidParent else {
Self.logger.info("Not handling invalid parent in change enumeration")
observer.finishEnumeratingWithError(error)
return

Check warning on line 741 in Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift

View check run for this annotation

Codecov / codecov/patch

Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift#L739-L741

Added lines #L739 - L741 were not covered by tests
}
do {
let metadata = try await Self.attemptInvalidParentRecovery(
error: error,
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager
)
var modifiedNewMetadatas = newMetadatas
modifiedNewMetadatas?.append(metadata)
Self.completeChangesObserver(
observer,
anchor: anchor,
enumeratedItemIdentifier: enumeratedItemIdentifier,
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
newMetadatas: modifiedNewMetadatas,
updatedMetadatas: updatedMetadatas,
deletedMetadatas: deletedMetadatas,
handleInvalidParent: false
)
} catch let error {
observer.finishEnumeratingWithError(error)

Check warning on line 765 in Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift

View check run for this annotation

Codecov / codecov/patch

Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift#L765

Added line #L765 was not covered by tests
}
}
}
}

private static func attemptInvalidParentRecovery(
error: NSError,
account: Account,
remoteInterface: RemoteInterface,
dbManager: FilesDatabaseManager
) async throws -> SendableItemMetadata {
Self.logger.info("Attempting recovery from invalid parent identifier.")
// Try to recover from errors involving missing metadata for a parent
let userInfoKey =
FilesDatabaseManager.ErrorUserInfoKey.missingParentServerUrlAndFileName.rawValue
guard let urlToEnumerate = (error as NSError).userInfo[userInfoKey] as? String else {
Self.logger.fault("No missing parent server url and filename in error user info.")
assert(false)
throw NSError()

Check warning on line 784 in Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift

View check run for this annotation

Codecov / codecov/patch

Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift#L782-L784

Added lines #L782 - L784 were not covered by tests
}

Self.logger.info(
"Recovering from invalid parent identifier at \(urlToEnumerate, privacy: .public)"
)
let (metadatas, _, _, _, error) = await Enumerator.readServerUrl(
urlToEnumerate,
account: account,
remoteInterface: remoteInterface,
dbManager: dbManager,
depth: .target
)
guard error == nil || error == .success, let metadata = metadatas?.first else {
Self.logger.error(
"""
Problem retrieving parent for metadata.
Error: \(error?.errorDescription ?? "NONE", privacy: .public)
Metadatas: \(metadatas?.count ?? -1, privacy: .public)
"""
)
throw error?.fileProviderError ?? NSFileProviderError(.cannotSynchronize)

Check warning on line 805 in Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift

View check run for this annotation

Codecov / codecov/patch

Sources/NextcloudFileProviderKit/Enumeration/Enumerator.swift#L798-L805

Added lines #L798 - L805 were not covered by tests
}
// Provide it to the caller method so it can ingest it into the database and fix future errs
return metadata
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,19 @@ extension Array {
}

func concurrentChunkedCompactMap<T>(
into size: Int = defaultChunkSize, transform: @escaping (Element) -> T?
) async -> [T] {
await withTaskGroup(of: [T].self) { group in
into size: Int = defaultChunkSize, transform: @escaping (Element) throws -> T?
) async throws -> [T] {
try await withThrowingTaskGroup(of: [T].self) { group in
var results = [T]()
results.reserveCapacity(self.count)

for chunk in chunked(into: size) {
group.addTask {
return chunk.compactMap { transform($0) }
return try chunk.compactMap { try transform($0) }
}
}

for await chunkResult in group {
for try await chunkResult in group {
results += chunkResult
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@
handlingCollisionAgainstItemInRemotePath problemRemotePath: String,
dbManager: FilesDatabaseManager,
remoteInterface: RemoteInterface
) -> Error? {
) async -> Error? {

Check warning on line 86 in Sources/NextcloudFileProviderKit/Extensions/NKError+Extensions.swift

View check run for this annotation

Codecov / codecov/patch

Sources/NextcloudFileProviderKit/Extensions/NKError+Extensions.swift#L86

Added line #L86 was not covered by tests
guard fileProviderError?.code == .filenameCollision else {
return fileProviderError as Error?
}
guard let collidingItemMetadata = dbManager.itemMetadata(
account: dbManager.account.ncKitAccount, locatedAtRemoteUrl: problemRemotePath
), let collidingItem = Item.storedItem(
), let collidingItem = await Item.storedItem(

Check warning on line 92 in Sources/NextcloudFileProviderKit/Extensions/NKError+Extensions.swift

View check run for this annotation

Codecov / codecov/patch

Sources/NextcloudFileProviderKit/Extensions/NKError+Extensions.swift#L92

Added line #L92 was not covered by tests
identifier: .init(collidingItemMetadata.ocId),
account: dbManager.account,
remoteInterface: remoteInterface,
Expand Down
6 changes: 3 additions & 3 deletions Sources/NextcloudFileProviderKit/Item/Item+Create.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
\(createError.errorDescription, privacy: .public)
"""
)
return (nil, createError.fileProviderError(
return (nil, await createError.fileProviderError(

Check warning on line 47 in Sources/NextcloudFileProviderKit/Item/Item+Create.swift

View check run for this annotation

Codecov / codecov/patch

Sources/NextcloudFileProviderKit/Item/Item+Create.swift#L47

Added line #L47 was not covered by tests
handlingCollisionAgainstItemInRemotePath: remotePath,
dbManager: dbManager,
remoteInterface: remoteInterface
Expand Down Expand Up @@ -79,7 +79,7 @@
\(readError.errorDescription, privacy: .public)
"""
)
return (nil, readError.fileProviderError(
return (nil, await readError.fileProviderError(

Check warning on line 82 in Sources/NextcloudFileProviderKit/Item/Item+Create.swift

View check run for this annotation

Codecov / codecov/patch

Sources/NextcloudFileProviderKit/Item/Item+Create.swift#L82

Added line #L82 was not covered by tests
handlingCollisionAgainstItemInRemotePath: remotePath,
dbManager: dbManager,
remoteInterface: remoteInterface
Expand Down Expand Up @@ -151,7 +151,7 @@
received ocId: \(ocId ?? "empty", privacy: .public)
"""
)
return (nil, error.fileProviderError(
return (nil, await error.fileProviderError(

Check warning on line 154 in Sources/NextcloudFileProviderKit/Item/Item+Create.swift

View check run for this annotation

Codecov / codecov/patch

Sources/NextcloudFileProviderKit/Item/Item+Create.swift#L154

Added line #L154 was not covered by tests
handlingCollisionAgainstItemInRemotePath: remotePath,
dbManager: dbManager,
remoteInterface: remoteInterface
Expand Down
6 changes: 4 additions & 2 deletions Sources/NextcloudFileProviderKit/Item/Item+Fetch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,10 @@ public extension Item {

dbManager.addItemMetadata(updatedMetadata)

guard let parentItemIdentifier = dbManager.parentItemIdentifierFromMetadata(
updatedMetadata
guard let parentItemIdentifier = await dbManager.parentItemIdentifierWithRemoteFallback(
fromMetadata: metadata,
remoteInterface: remoteInterface,
account: account
) else {
Self.logger.error(
"""
Expand Down
Loading