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

Auto trash emptying #1014

Merged
merged 9 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 13 additions & 3 deletions Zotero.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,8 @@
B3501F5425139B40007961DB /* Rounding+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B340692124A60D6A009ECE48 /* Rounding+Extensions.swift */; };
B3518DA72AEBCB5E00D983B4 /* SettingsResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3518DA62AEBCB5E00D983B4 /* SettingsResponseSpec.swift */; };
B351BD0E25EF7E78000451E2 /* ItemAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B351BD0D25EF7E78000451E2 /* ItemAction.swift */; };
B352FFCC2CBE944400D0887B /* AutoEmptyTrashDbRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B352FFCB2CBE943B00D0887B /* AutoEmptyTrashDbRequest.swift */; };
B352FFCD2CBE944400D0887B /* AutoEmptyTrashDbRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B352FFCB2CBE943B00D0887B /* AutoEmptyTrashDbRequest.swift */; };
B353F1FB242E21880062EE24 /* ResetTranslatorsDbRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B353F1FA242E21880062EE24 /* ResetTranslatorsDbRequest.swift */; };
B353F1FC242E23680062EE24 /* ResetTranslatorsDbRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B353F1FA242E21880062EE24 /* ResetTranslatorsDbRequest.swift */; };
B353F204242E52610062EE24 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = B353F202242E52610062EE24 /* Database.swift */; };
Expand Down Expand Up @@ -1018,6 +1020,7 @@
B3D32A4D286C77850075C6D7 /* ItemSortingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D32A4C286C77850075C6D7 /* ItemSortingView.swift */; };
B3D3FCA9267762EC008E243A /* ExportLocaleReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D3FCA8267762EC008E243A /* ExportLocaleReader.swift */; };
B3D4159E2948B3DA004ABB3E /* FixNotesWithEmptyTitlesDbRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D4159D2948B3DA004ABB3E /* FixNotesWithEmptyTitlesDbRequest.swift */; };
B3D427D62CB67EFC0058453A /* AutoEmptyTrashController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D427D52CB67EEA0058453A /* AutoEmptyTrashController.swift */; };
B3D58D5625ED856F00D8FA31 /* DebugLogUploadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D58D5525ED856F00D8FA31 /* DebugLogUploadRequest.swift */; };
B3D58D5B25ED861500D8FA31 /* DebugLogUploadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D58D5525ED856F00D8FA31 /* DebugLogUploadRequest.swift */; };
B3D58D6225EE26A600D8FA31 /* DebugResponseParserDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3D58D6125EE26A600D8FA31 /* DebugResponseParserDelegate.swift */; };
Expand Down Expand Up @@ -1665,6 +1668,7 @@
B34F9FA423743C42004ED34C /* ItemTitleFormatterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemTitleFormatterSpec.swift; sourceTree = "<group>"; };
B3518DA62AEBCB5E00D983B4 /* SettingsResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsResponseSpec.swift; sourceTree = "<group>"; };
B351BD0D25EF7E78000451E2 /* ItemAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemAction.swift; sourceTree = "<group>"; };
B352FFCB2CBE943B00D0887B /* AutoEmptyTrashDbRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoEmptyTrashDbRequest.swift; sourceTree = "<group>"; };
B353F1FA242E21880062EE24 /* ResetTranslatorsDbRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResetTranslatorsDbRequest.swift; sourceTree = "<group>"; };
B353F202242E52610062EE24 /* Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = "<group>"; };
B355B12B2850B6C400BAE2C5 /* TableViewDiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewDiffableDataSource.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2008,6 +2012,7 @@
B3D32A4C286C77850075C6D7 /* ItemSortingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemSortingView.swift; sourceTree = "<group>"; };
B3D3FCA8267762EC008E243A /* ExportLocaleReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportLocaleReader.swift; sourceTree = "<group>"; };
B3D4159D2948B3DA004ABB3E /* FixNotesWithEmptyTitlesDbRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FixNotesWithEmptyTitlesDbRequest.swift; sourceTree = "<group>"; };
B3D427D52CB67EEA0058453A /* AutoEmptyTrashController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoEmptyTrashController.swift; sourceTree = "<group>"; };
B3D58D5525ED856F00D8FA31 /* DebugLogUploadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugLogUploadRequest.swift; sourceTree = "<group>"; };
B3D58D6125EE26A600D8FA31 /* DebugResponseParserDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugResponseParserDelegate.swift; sourceTree = "<group>"; };
B3D58D6A25EE437700D8FA31 /* CircularProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2296,6 +2301,7 @@
B30B40642491222000FAAF6D /* AttachmentCreator.swift */,
B377F2A4249373F300022943 /* AttachmentFileCleanupController.swift */,
B30BA03D297ED50B0005021B /* AttributedTagStringGenerator.swift */,
B3D427D52CB67EEA0058453A /* AutoEmptyTrashController.swift */,
B31D4A712767840800E22DCC /* BackgroundTaskController.swift */,
B3BD2BB225C98D2900275EF9 /* BackgroundTimer.swift */,
B3C8DD8127B502960084E1AD /* CollectionTreeBuilder.swift */,
Expand All @@ -2310,19 +2316,20 @@
B305648123FC051E003304F2 /* Formatter.swift */,
B379D9312BB30E6600AF5025 /* FullSyncDebugger.swift */,
B367330C24ACB63300E0CDA8 /* HtmlAttributedStringConverter.swift */,
618404252A4456A9005AAF22 /* IdentifierLookupController.swift */,
B307A2722704A87D005986B3 /* IdleTimerController.swift */,
61639F842AE03B8500026003 /* InstantPresenter.swift */,
B30564B923FC051E003304F2 /* ItemTitleFormatter.swift */,
B39649172869B0D0000BCB6C /* ISBNParser.swift */,
B30564B923FC051E003304F2 /* ItemTitleFormatter.swift */,
B305648023FC051E003304F2 /* KeyGenerator.swift */,
B38CD21D241128D4004299EA /* KeysResponseProcessor.swift */,
B305649E23FC051E003304F2 /* Licenses.swift */,
B3A17D1827FC33B800322CAD /* LowPowerModeController.swift */,
B3C43C1F28589F300007076D /* NotePreviewGenerator.swift */,
B305646C23FC051E003304F2 /* ObjectUserChangeObserver.swift */,
61E24DCB2ABB385E00D75F50 /* OpenItemsController.swift */,
B34A9F6325BF1ABB007C9A4A /* PDFDocumentExporter.swift */,
B32B8A562B18A08900A9A741 /* PDFThumbnailController.swift */,
61E24DCB2ABB385E00D75F50 /* OpenItemsController.swift */,
B3C6D551261C9F2E0068B9FE /* PlaceholderTextViewDelegate.swift */,
B378F4CC242CD45700B88A05 /* RepoParserDelegate.swift */,
B305646A23FC051E003304F2 /* RItemLocaleController.swift */,
Expand All @@ -2337,7 +2344,6 @@
B36A988C2428E059005D5790 /* TranslatorsAndStylesController.swift */,
B3972688247D403200A8B469 /* UrlDetector.swift */,
B324276C25C81F2000567504 /* WebSocketController.swift */,
618404252A4456A9005AAF22 /* IdentifierLookupController.swift */,
B3F9A4CA2B04F28400684030 /* WebViewEncoder.swift */,
B3B557152C884DD200BD6325 /* ZoteroURIConverter.swift */,
);
Expand All @@ -2360,6 +2366,7 @@
B310FA4429E5765800FA2F15 /* AddTagsToItemDbRequest.swift */,
B305646123FC051E003304F2 /* AssignItemsToCollectionsDbRequest.swift */,
B305CEBA29E6E67600B9E2B4 /* AssignItemsToTagDbRequest.swift */,
B352FFCB2CBE943B00D0887B /* AutoEmptyTrashDbRequest.swift */,
B37D21352AD6D8AB004A6496 /* CancelParentCreationDbRequest.swift */,
B358D3B3279590C200A67054 /* CheckAnyItemIsInTrashDbRequest.swift */,
B305644423FC051E003304F2 /* CheckItemIsChangedDbRequest.swift */,
Expand Down Expand Up @@ -4768,6 +4775,7 @@
B3B1C5842664E23A00883597 /* ReadItemsForUploadDbRequest.swift in Sources */,
B39649182869B0D0000BCB6C /* ISBNParser.swift in Sources */,
B305662123FC051F003304F2 /* SubmitDeletionSyncAction.swift in Sources */,
B352FFCD2CBE944400D0887B /* AutoEmptyTrashDbRequest.swift in Sources */,
B305661B23FC051E003304F2 /* SyncVersionsSyncAction.swift in Sources */,
B3593F49241A61C700760E20 /* LibrariesAction.swift in Sources */,
B3E8FE032714292E00F51458 /* StorageSettingsState.swift in Sources */,
Expand Down Expand Up @@ -5069,6 +5077,7 @@
B30565E623FC051E003304F2 /* SchemaController.swift in Sources */,
B339459126DE6C2A00E59A02 /* AttachmentFileDeletedNotification.swift in Sources */,
B3593F62241A62DD00760E20 /* DetailCoordinator.swift in Sources */,
B3D427D62CB67EFC0058453A /* AutoEmptyTrashController.swift in Sources */,
B30565EB23FC051E003304F2 /* Controllers.swift in Sources */,
B3A351E12715784A002E597A /* WebDavDownloadRequest.swift in Sources */,
B305CEBB29E6E67600B9E2B4 /* AssignItemsToTagDbRequest.swift in Sources */,
Expand Down Expand Up @@ -5618,6 +5627,7 @@
B3E8B25027DA39B0001825F8 /* SplitAnnotationsDbRequest.swift in Sources */,
B37D8E6A24DC2BF300F526C5 /* CollectionRow.swift in Sources */,
B3868541270DC3AA0068A022 /* WebDavScheme.swift in Sources */,
B352FFCC2CBE944400D0887B /* AutoEmptyTrashDbRequest.swift in Sources */,
B331F9AF2653CEA00099F6A6 /* ReadGroupDbRequest.swift in Sources */,
B33E8A4B27B6A39100CBC7DE /* CollectionCell.swift in Sources */,
B3DDC0CC2667825E00B2DFD1 /* RegularExpression+Extensions.swift in Sources */,
Expand Down
2 changes: 2 additions & 0 deletions Zotero/Assets/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@
"settings.general.show_subcollections_title" = "Show Items from Subcollections";
"settings.general.show_collection_item_counts" = "Show collection sizes";
"settings.general.open_links_in_external_browser" = "Open links in external browser";
"settings.general.autoempty_title" = "Delete Items in Trash";
"settings.general.never" = "Never";
"settings.item_count" = "Item count";
"settings.item_count_subtitle" = "Show item count for all collections.";
"settings.sync.title" = "Account";
Expand Down
18 changes: 17 additions & 1 deletion Zotero/Assets/en.lproj/Localizable.stringsdict
Original file line number Diff line number Diff line change
Expand Up @@ -195,5 +195,21 @@
<string>Could not delete %d files from your WebDAV server</string>
</dict>
</dict>
<key>settings.general.after_x_days</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@after_x_days@</string>
<key>after_x_days</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>After 1 Day</string>
<key>other</key>
<string>After %d Days</string>
</dict>
</dict>
</dict>
</plist>
</plist>
41 changes: 41 additions & 0 deletions Zotero/Controllers/AutoEmptyTrashController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// AutoEmptyTrashController.swift
// Zotero
//
// Created by Michal Rentka on 09.10.2024.
// Copyright © 2024 Corporation for Digital Scholarship. All rights reserved.
//

import CocoaLumberjackSwift

final class AutoEmptyTrashController {
private unowned let dbStorage: DbStorage
private let queue: DispatchQueue

init(dbStorage: DbStorage) {
self.dbStorage = dbStorage
queue = DispatchQueue(label: "org.zotero.AutoEmptyTrashController.queue", qos: .utility)
}

func autoEmptyIfNeeded() {
if Defaults.shared.trashAutoEmptyThreshold == 0 {
DDLogInfo("AutoEmptyTrashController: auto emptying disabled")
return
}

// Auto empty trash once a day
guard Date.now.timeIntervalSince(Defaults.shared.trashLastAutoEmptyDate) >= 86400 else { return }

DDLogInfo("AutoEmptyTrashController: perform auto empty")
Defaults.shared.trashLastAutoEmptyDate = .now

queue.asyncAfter(deadline: .now() + .seconds(1)) { [weak self] in
guard let self else { return }
do {
try dbStorage.perform(request: AutoEmptyTrashDbRequest(libraryId: .custom(.myLibrary)), on: queue)
} catch let error {
DDLogError("AutoEmptyTrashController: can't empty trash - \(error)")
}
}
}
}
33 changes: 18 additions & 15 deletions Zotero/Controllers/Controllers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ final class Controllers {

/// Global controllers for logged in user
final class UserControllers {
let autoEmptyController: AutoEmptyTrashController
let syncScheduler: (SynchronizationScheduler & WebSocketScheduler)
let changeObserver: ObjectUserChangeObserver
let dbStorage: DbStorage
Expand Down Expand Up @@ -376,16 +377,17 @@ final class UserControllers {
}
})

autoEmptyController = AutoEmptyTrashController(dbStorage: dbStorage)
self.isFirstLaunch = isFirstLaunch
self.dbStorage = dbStorage
self.syncScheduler = SyncScheduler(controller: syncController, retryIntervals: DelayIntervals.retry)
syncScheduler = SyncScheduler(controller: syncController, retryIntervals: DelayIntervals.retry)
self.webDavController = webDavController
self.changeObserver = RealmObjectUserChangeObserver(dbStorage: dbStorage)
self.itemLocaleController = RItemLocaleController(schemaController: controllers.schemaController, dbStorage: dbStorage)
changeObserver = RealmObjectUserChangeObserver(dbStorage: dbStorage)
itemLocaleController = RItemLocaleController(schemaController: controllers.schemaController, dbStorage: dbStorage)
self.backgroundUploadObserver = backgroundUploadObserver
self.fileDownloader = fileDownloader
self.remoteFileDownloader = RemoteAttachmentDownloader(apiClient: controllers.apiClient, fileStorage: controllers.fileStorage)
self.identifierLookupController = IdentifierLookupController(
remoteFileDownloader = RemoteAttachmentDownloader(apiClient: controllers.apiClient, fileStorage: controllers.fileStorage)
identifierLookupController = IdentifierLookupController(
dbStorage: dbStorage,
fileStorage: controllers.fileStorage,
translatorsController: controllers.translatorsAndStylesController,
Expand All @@ -395,23 +397,24 @@ final class UserControllers {
)
self.webSocketController = webSocketController
self.fileCleanupController = fileCleanupController
self.citationController = CitationController(
citationController = CitationController(
stylesController: controllers.translatorsAndStylesController,
fileStorage: controllers.fileStorage,
dbStorage: dbStorage,
bundledDataStorage: controllers.bundledDataStorage
)
self.translatorsAndStylesController = controllers.translatorsAndStylesController
translatorsAndStylesController = controllers.translatorsAndStylesController
fullSyncDebugger = FullSyncDebugger(syncScheduler: syncScheduler, debugLogging: controllers.debugLogging, sessionController: controllers.sessionController)
self.idleTimerController = controllers.idleTimerController
self.customUrlController = CustomURLController(dbStorage: dbStorage, fileStorage: controllers.fileStorage)
self.lastBuildNumber = controllers.lastBuildNumber
self.disposeBag = DisposeBag()
idleTimerController = controllers.idleTimerController
customUrlController = CustomURLController(dbStorage: dbStorage, fileStorage: controllers.fileStorage)
lastBuildNumber = controllers.lastBuildNumber
disposeBag = DisposeBag()
}

/// Connects to websocket to monitor changes and performs initial sync.
fileprivate func enableSync(apiKey: String) {
self.itemLocaleController.loadLocale()
itemLocaleController.loadLocale()
autoEmptyController.autoEmptyIfNeeded()

// Enable idleTimerController before syncScheduler inProgress observation starts
idleTimerController.enable()
Expand Down Expand Up @@ -443,7 +446,7 @@ final class UserControllers {
.disposed(by: disposeBag)

// Observe local changes to start sync
self.changeObserver.observable
changeObserver.observable
.debounce(.seconds(3), scheduler: MainScheduler.instance)
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] changedLibraries in
Expand All @@ -452,7 +455,7 @@ final class UserControllers {
.disposed(by: self.disposeBag)

// Observe remote changes to start sync/translator update
self.webSocketController.observable
webSocketController.observable
.debounce(.seconds(3), scheduler: MainScheduler.instance)
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] change in
Expand All @@ -467,7 +470,7 @@ final class UserControllers {
.disposed(by: self.disposeBag)

// Connect to websockets and start sync
self.webSocketController.connect(apiKey: apiKey, completed: { [weak self] in
webSocketController.connect(apiKey: apiKey, completed: { [weak self] in
guard let self = self else { return }
// Call this before sync so that background uploads are updated and taken care of by sync if needed.
self.backgroundUploadObserver.updateSessions()
Expand Down
41 changes: 41 additions & 0 deletions Zotero/Controllers/Database/Requests/AutoEmptyTrashDbRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// AutoEmptyTrashDbRequest.swift
// Zotero
//
// Created by Michal Rentka on 15.10.2024.
// Copyright © 2024 Corporation for Digital Scholarship. All rights reserved.
//

import CocoaLumberjackSwift
import RealmSwift

struct AutoEmptyTrashDbRequest: DbRequest {
let libraryId: LibraryIdentifier

var needsWrite: Bool { return true }

func process(in database: Realm) throws {
let threshold = Defaults.shared.trashAutoEmptyThreshold
var count = 0
database.objects(RItem.self).filter(.items(for: .custom(.trash), libraryId: libraryId)).filter("trashDate != nil").forEach {
guard let date = $0.trashDate, shouldDelete(date: date) else { return }
$0.deleted = true
$0.changeType = .user
count += 1
}
DDLogInfo("Auto emptied \(count) items")
count = 0
database.objects(RCollection.self).filter(.trashedCollections(in: .custom(.myLibrary))).filter("trashDate != nil").forEach {
guard let date = $0.trashDate, shouldDelete(date: date) else { return }
$0.deleted = true
$0.changeType = .user
count += 1
}
DDLogInfo("Auto emptied \(count) collections")

func shouldDelete(date: Date) -> Bool {
let daysSinceTrashed = Int(Date.now.timeIntervalSince(date) / 86400)
return daysSinceTrashed >= threshold
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ struct MarkItemsAsTrashedDbRequest: DbRequest {

func process(in database: Realm) throws {
let items = database.objects(RItem.self).filter(.keys(self.keys, in: self.libraryId))
let now = Date.now
items.forEach { item in
item.trash = self.trashed
item.trash = trashed
item.trashDate = trashed ? now : nil
item.dateModified = now
item.changeType = .user
item.changes.append(RObjectChange.create(changes: RItemChanges.trash))
}
Expand Down
Loading