Skip to content

Commit

Permalink
Auto trash emptying (#1014)
Browse files Browse the repository at this point in the history
  • Loading branch information
michalrentka authored Oct 16, 2024
1 parent cc3bd88 commit 3bdad24
Show file tree
Hide file tree
Showing 26 changed files with 259 additions and 71 deletions.
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

0 comments on commit 3bdad24

Please sign in to comment.