Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix freezes #3246

Merged
merged 10 commits into from
Dec 23, 2024
2 changes: 1 addition & 1 deletion Brand/Database.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ import Foundation
// Database Realm
//
let databaseName = "nextcloud.realm"
let databaseSchemaVersion: UInt64 = 367
let databaseSchemaVersion: UInt64 = 368
54 changes: 50 additions & 4 deletions Nextcloud.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions iOSClient/Data/NCManageDatabase+Capabilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ extension NCManageDatabase {
}

struct Capabilities: Codable {
let downloadLimit: DownloadLimit?
let filessharing: FilesSharing?
let theming: Theming?
let endtoendencryption: EndToEndEncryption?
Expand All @@ -102,6 +103,7 @@ extension NCManageDatabase {
let assistant: Assistant?

enum CodingKeys: String, CodingKey {
case downloadLimit = "downloadlimit"
case filessharing = "files_sharing"
case theming
case endtoendencryption = "end-to-end-encryption"
Expand All @@ -112,6 +114,11 @@ extension NCManageDatabase {
case assistant
}

struct DownloadLimit: Codable {
let enabled: Bool?
let defaultLimit: Int?
}

struct FilesSharing: Codable {
let apienabled: Bool?
let groupsharing: Bool?
Expand Down Expand Up @@ -327,6 +334,8 @@ extension NCManageDatabase {
capabilities.capabilityFileSharingInternalExpireDateDays = data.capabilities.filessharing?.ncpublic?.expiredateinternal?.days ?? 0
capabilities.capabilityFileSharingRemoteExpireDateEnforced = data.capabilities.filessharing?.ncpublic?.expiredateremote?.enforced ?? false
capabilities.capabilityFileSharingRemoteExpireDateDays = data.capabilities.filessharing?.ncpublic?.expiredateremote?.days ?? 0
capabilities.capabilityFileSharingDownloadLimit = data.capabilities.downloadLimit?.enabled ?? false
capabilities.capabilityFileSharingDownloadLimitDefaultLimit = data.capabilities.downloadLimit?.defaultLimit ?? 1

capabilities.capabilityThemingColor = data.capabilities.theming?.color ?? ""
capabilities.capabilityThemingColorElement = data.capabilities.theming?.colorelement ?? ""
Expand Down
95 changes: 95 additions & 0 deletions iOSClient/Data/NCManageDatabase+DownloadLimit.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: Nextcloud GmbH
// SPDX-FileCopyrightText: 2024 Iva Horn
// SPDX-License-Identifier: GPL-3.0-or-later

import Foundation
import NextcloudKit
import RealmSwift

///
/// Data model for storing information about download limits of shares.
///
class tableDownloadLimit: Object {
///
/// The number of downloads which already happened.
///
@Persisted
@objc dynamic var count: Int = 0

///
/// Total number of allowed downloads.
///
@Persisted
@objc dynamic var limit: Int = 0

///
/// The token identifying the related share.
///
@Persisted(primaryKey: true)
@objc dynamic var token: String = ""
}

extension NCManageDatabase {
///
/// Create a new download limit object in the database.
///
@discardableResult
func createDownloadLimit(count: Int, limit: Int, token: String) throws -> tableDownloadLimit? {
let downloadLimit = tableDownloadLimit()
downloadLimit.count = count
downloadLimit.limit = limit
downloadLimit.token = token

do {
let realm = try Realm()

try realm.write {
realm.add(downloadLimit, update: .all)
}
} catch let error as NSError {
NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)")
}

return downloadLimit
}

///
/// Delete an existing download limit object identified by the token of its related share.
///
/// - Parameter token: The `token` of the associated ``Nextcloud/tableShare/token``.
///
func deleteDownloadLimit(byShareToken token: String) throws {
do {
let realm = try Realm()

try realm.write {
let result = realm.objects(tableDownloadLimit.self).filter("token == %@", token)
realm.delete(result)
}
} catch let error as NSError {
NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not write to database: \(error)")
}
}

///
/// Retrieve a download limit by the token of the associated ``Nextcloud/tableShare/token``.
///
/// - Parameter token: The `token` of the associated ``tableShare``.
///
func getDownloadLimit(byShareToken token: String) throws -> tableDownloadLimit? {
do {
let realm = try Realm()
let predicate = NSPredicate(format: "token == %@", token)

guard let result = realm.objects(tableDownloadLimit.self).filter(predicate).first else {
return nil
}

return result
} catch let error as NSError {
NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)")
}

return nil
}
}
46 changes: 40 additions & 6 deletions iOSClient/Data/NCManageDatabase+Share.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,12 @@ class tableShareV2: Object {
@objc dynamic var primaryKey = ""
@objc dynamic var sendPasswordByTalk: Bool = false
@objc dynamic var serverUrl = ""

///
/// See [OCS Share API documentation](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-share-api.html) for semantic definitions of the different possible values.
///
@objc dynamic var shareType: Int = 0

@objc dynamic var shareWith = ""
@objc dynamic var shareWithDisplayname = ""
@objc dynamic var storage: Int = 0
Expand Down Expand Up @@ -146,22 +151,48 @@ extension NCManageDatabase {
return []
}

///
/// Fetch all available shares of an item identified by the given metadata.
///
/// - Returns: A tuple consisting of the first public share link and any _additional_ shares that might be there.
/// It is possible that there is no public share link but still shares of other types.
/// In the latter case, all shares are returned as the second tuple value.
///
func getTableShares(metadata: tableMetadata) -> (firstShareLink: tableShare?, share: [tableShare]?) {
do {
let realm = try Realm()
realm.refresh()
let sortProperties = [SortDescriptor(keyPath: "shareType", ascending: false), SortDescriptor(keyPath: "idShare", ascending: false)]
let firstShareLink = realm.objects(tableShare.self).filter("account == %@ AND serverUrl == %@ AND fileName == %@ AND shareType == 3", metadata.account, metadata.serverUrl, metadata.fileName).first

let sortProperties = [
SortDescriptor(keyPath: "shareType", ascending: false),
SortDescriptor(keyPath: "idShare", ascending: false)
]

let firstShareLink = realm
.objects(tableShare.self)
.filter("account == %@ AND serverUrl == %@ AND fileName == %@ AND shareType == 3", metadata.account, metadata.serverUrl, metadata.fileName)
.first

if let firstShareLink = firstShareLink {
let results = realm.objects(tableShare.self).filter("account == %@ AND serverUrl == %@ AND fileName == %@ AND idShare != %d", metadata.account, metadata.serverUrl, metadata.fileName, firstShareLink.idShare).sorted(by: sortProperties)
return(firstShareLink: tableShare.init(value: firstShareLink), share: Array(results.map { tableShare.init(value: $0) }))
let results = realm
.objects(tableShare.self)
.filter("account == %@ AND serverUrl == %@ AND fileName == %@ AND idShare != %d", metadata.account, metadata.serverUrl, metadata.fileName, firstShareLink.idShare)
.sorted(by: sortProperties)

return (firstShareLink: tableShare.init(value: firstShareLink), share: Array(results.map { tableShare.init(value: $0) }))
} else {
let results = realm.objects(tableShare.self).filter("account == %@ AND serverUrl == %@ AND fileName == %@", metadata.account, metadata.serverUrl, metadata.fileName).sorted(by: sortProperties)
return(firstShareLink: firstShareLink, share: Array(results.map { tableShare.init(value: $0) }))
let results = realm
.objects(tableShare.self)
.filter("account == %@ AND serverUrl == %@ AND fileName == %@", metadata.account, metadata.serverUrl, metadata.fileName)
.sorted(by: sortProperties)

return (firstShareLink: firstShareLink, share: Array(results.map { tableShare.init(value: $0) }))
}

} catch let error as NSError {
NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not access database: \(error)")
}

return (nil, nil)
}

Expand Down Expand Up @@ -190,6 +221,9 @@ extension NCManageDatabase {
return []
}

///
/// Fetch all shares of a file regardless of type.
///
func getTableShares(account: String, serverUrl: String, fileName: String) -> [tableShare] {
do {
let realm = try Realm()
Expand Down
3 changes: 2 additions & 1 deletion iOSClient/Data/NCManageDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ class NCManageDatabase: NSObject {
tableDashboardWidget.self,
tableDashboardWidgetButton.self,
NCDBLayoutForView.self,
TableSecurityGuardDiagnostics.self]
TableSecurityGuardDiagnostics.self,
tableDownloadLimit.self]

// Disable file protection for directory DB
// https://docs.mongodb.com/realm/sdk/ios/examples/configure-and-open-a-realm/#std-label-ios-open-a-local-realm
Expand Down
22 changes: 14 additions & 8 deletions iOSClient/Files/NCFiles.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class NCFiles: NCCollectionViewCommon {
internal var fileNameBlink: String?
internal var fileNameOpen: String?
internal var matadatasHash: String = ""
internal var reloadDataSourceInProgress: Bool = false
internal var semaphoreReloadDataSource = DispatchSemaphore(value: 1)

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
Expand Down Expand Up @@ -122,12 +122,16 @@ class NCFiles: NCCollectionViewCommon {
// MARK: - DataSource

override func reloadDataSource() {
guard !isSearchingMode,
!reloadDataSourceInProgress
guard !isSearchingMode
else {
return super.reloadDataSource()
}
reloadDataSourceInProgress = true

// This is only a fail safe "dead lock", I don't think the timeout will ever be called but at least nothing gets stuck, if after 5 sec. (which is a long time in this routine), the semaphore is still locked
//
if self.semaphoreReloadDataSource.wait(timeout: .now() + 5) == .timedOut {
self.semaphoreReloadDataSource.signal()
}

var predicate = self.defaultPredicate
let predicateDirectory = NSPredicate(format: "account == %@ AND serverUrl == %@", session.account, self.serverUrl)
Expand All @@ -145,14 +149,16 @@ class NCFiles: NCCollectionViewCommon {
self.dataSource = NCCollectionViewDataSource(metadatas: metadatas, layoutForView: layoutForView)

if metadatas.isEmpty {
reloadDataSourceInProgress = false
self.semaphoreReloadDataSource.signal()
return super.reloadDataSource()
}

self.dataSource.caching(metadatas: metadatas, dataSourceMetadatas: dataSourceMetadatas) { updated in
self.reloadDataSourceInProgress = false
if updated || self.isNumberOfItemsInAllSectionsNull || self.numberOfItemsInAllSections != metadatas.count {
super.reloadDataSource()
self.semaphoreReloadDataSource.signal()
DispatchQueue.main.async {
if updated || self.isNumberOfItemsInAllSectionsNull || self.numberOfItemsInAllSections != metadatas.count {
super.reloadDataSource()
}
}
}
}
Expand Down
19 changes: 17 additions & 2 deletions iOSClient/Media/NCMedia.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ class NCMedia: UIViewController {
@IBOutlet weak var menuButton: UIButton!
@IBOutlet weak var gradientView: UIView!

let semaphoreSearchMedia = DispatchSemaphore(value: 1)
let semaphoreNotificationCenter = DispatchSemaphore(value: 1)

let layout = NCMediaLayout()
var layoutType = NCGlobal.shared.mediaLayoutRatio
var documentPickerViewController: NCDocumentPickerViewController?
Expand Down Expand Up @@ -257,13 +260,25 @@ class NCMedia: UIViewController {
return
}

// This is only a fail safe "dead lock", I don't think the timeout will ever be called but at least nothing gets stuck, if after 5 sec. (which is a long time in this routine), the semaphore is still locked
//
if self.semaphoreNotificationCenter.wait(timeout: .now() + 5) == .timedOut {
self.semaphoreNotificationCenter.signal()
}

if error.errorCode == self.global.errorResourceNotFound,
let ocId = userInfo["ocId"] as? String {
self.database.deleteMetadataOcId(ocId)
self.loadDataSource()
self.loadDataSource {
self.semaphoreNotificationCenter.signal()
}
} else if error != .success {
NCContentPresenter().showError(error: error)
self.loadDataSource()
self.loadDataSource {
self.semaphoreNotificationCenter.signal()
}
} else {
semaphoreNotificationCenter.signal()
}
}

Expand Down
7 changes: 5 additions & 2 deletions iOSClient/Media/NCMediaDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,13 @@ extension NCMedia {
NCNetworking.shared.downloadThumbnailQueue.operationCount == 0,
let tableAccount = database.getTableAccount(predicate: NSPredicate(format: "account == %@", session.account))
else { return }
self.searchMediaInProgress = true

let limit = max(self.collectionView.visibleCells.count * 3, 300)
let visibleCells = self.collectionView?.indexPathsForVisibleItems.sorted(by: { $0.row < $1.row }).compactMap({ self.collectionView?.cellForItem(at: $0) })

DispatchQueue.global(qos: .background).async {
self.semaphoreSearchMedia.wait()
self.searchMediaInProgress = true

var lessDate = Date.distantFuture
var greaterDate = Date.distantPast
let countMetadatas = self.dataSource.metadatas.count
Expand Down Expand Up @@ -156,6 +157,8 @@ extension NCMedia {
self.collectionViewReloadData()
}

self.semaphoreSearchMedia.signal()

DispatchQueue.main.async {
self.activityIndicator.stopAnimating()
self.searchMediaInProgress = false
Expand Down
1 change: 1 addition & 0 deletions iOSClient/Menu/NCShare+Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ extension NCShare {
advancePermission.share = tableShare(value: share)
advancePermission.oldTableShare = tableShare(value: share)
advancePermission.metadata = self.metadata
advancePermission.downloadLimit = try? self.database.getDownloadLimit(byShareToken: share.token)
navigationController.pushViewController(advancePermission, animated: true)
}
)
Expand Down
2 changes: 2 additions & 0 deletions iOSClient/NCCapabilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public class NCCapabilities: NSObject {
var capabilityFileSharingRemoteExpireDateEnforced: Bool = false
var capabilityFileSharingRemoteExpireDateDays: Int = 0
var capabilityFileSharingDefaultPermission: Int = 0
var capabilityFileSharingDownloadLimit: Bool = false
var capabilityFileSharingDownloadLimitDefaultLimit: Int = 1
var capabilityThemingColor: String = ""
var capabilityThemingColorElement: String = ""
var capabilityThemingColorText: String = ""
Expand Down
18 changes: 18 additions & 0 deletions iOSClient/Networking/NCNetworking+WebDAV.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,24 @@ extension NCNetworking {
let isDirectoryE2EE = self.utilityFileSystem.isDirectoryE2EE(file: file)
let metadata = self.database.convertFileToMetadata(file, isDirectoryE2EE: isDirectoryE2EE)

// Remove all known download limits from shares related to the given file.
// This avoids obsolete download limit objects to stay around.
// Afterwards create new download limits, should any such be returned for the known shares.

let shares = self.database.getTableShares(account: metadata.account, serverUrl: metadata.serverUrl, fileName: metadata.fileName)

do {
try shares.forEach { share in
try self.database.deleteDownloadLimit(byShareToken: share.token)

if let receivedDownloadLimit = file.downloadLimits.first(where: { $0.token == share.token }) {
try self.database.createDownloadLimit(count: receivedDownloadLimit.count, limit: receivedDownloadLimit.limit, token: receivedDownloadLimit.token)
}
}
} catch {
NextcloudKit.shared.nkCommonInstance.writeLog("[ERROR] Could not update download limits: \(error)")
}

completion(account, metadata, error)
}
}
Expand Down
Loading
Loading