Speed up emptying trash
Shows a progress meter, which allows for larger chunks and fewer refreshes, avoids unnecessary updating of parent items that are being deleted anyway, and skip re-sorting of modified items in the trash. Closes #1292, Emptying trash is slow
This commit is contained in:
parent
7935d01a1c
commit
3872e646ac
7 changed files with 101 additions and 53 deletions
|
@ -1160,7 +1160,7 @@ Zotero.DataObject.prototype.updateSynced = Zotero.Promise.coroutine(function* (s
|
|||
*/
|
||||
Zotero.DataObject.prototype.erase = Zotero.Promise.coroutine(function* (options = {}) {
|
||||
if (!options || typeof options != 'object') {
|
||||
throw new Error("'options' must be an object");
|
||||
throw new Error("'options' must be an object (" + typeof options + ")");
|
||||
}
|
||||
|
||||
var env = {
|
||||
|
|
|
@ -909,6 +909,7 @@ Zotero.DataObjects.prototype.getPrimaryDataSQLPart = function (part) {
|
|||
*
|
||||
* @param {Integer|Integer[]} ids - Object ids
|
||||
* @param {Object} [options] - See Zotero.DataObject.prototype.erase
|
||||
* @param {Function} [options.onProgress] - f(progress, progressMax)
|
||||
* @return {Promise}
|
||||
*/
|
||||
Zotero.DataObjects.prototype.erase = Zotero.Promise.coroutine(function* (ids, options = {}) {
|
||||
|
@ -920,6 +921,9 @@ Zotero.DataObjects.prototype.erase = Zotero.Promise.coroutine(function* (ids, op
|
|||
continue;
|
||||
}
|
||||
yield obj.erase(options);
|
||||
if (options.onProgress) {
|
||||
options.onProgress(i + 1, ids.length);
|
||||
}
|
||||
}
|
||||
this.unload(ids);
|
||||
}.bind(this));
|
||||
|
|
|
@ -3972,13 +3972,13 @@ Zotero.Item.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
|
|||
? (yield this.ObjectsClass.getByLibraryAndKeyAsync(this.libraryID, parentItem))
|
||||
: null;
|
||||
|
||||
if (parentItem) {
|
||||
if (parentItem && !env.options.skipParentRefresh) {
|
||||
Zotero.Notifier.queue('refresh', 'item', parentItem.id);
|
||||
}
|
||||
|
||||
// // Delete associated attachment files
|
||||
if (this.isAttachment()) {
|
||||
let linkMode = this.getAttachmentLinkMode();
|
||||
let linkMode = this.attachmentLinkMode;
|
||||
// If link only, nothing to delete
|
||||
if (linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
||||
try {
|
||||
|
@ -4005,7 +4005,9 @@ Zotero.Item.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
|
|||
for (let i=0; i<toDelete.length; i++) {
|
||||
let obj = yield this.ObjectsClass.getAsync(toDelete[i]);
|
||||
// Copy all options other than 'tx', which would cause a deadlock
|
||||
let options = {};
|
||||
let options = {
|
||||
skipParentRefresh: true
|
||||
};
|
||||
Object.assign(options, env.options);
|
||||
delete options.tx;
|
||||
yield obj.erase(options);
|
||||
|
@ -4029,7 +4031,7 @@ Zotero.Item.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
|
|||
|
||||
yield Zotero.DB.queryAsync('DELETE FROM items WHERE itemID=?', this.id);
|
||||
|
||||
if (parentItem) {
|
||||
if (parentItem && !env.options.skipParentRefresh) {
|
||||
yield parentItem.reload(['primaryData', 'childItems'], true);
|
||||
parentItem.clearBestAttachmentState();
|
||||
}
|
||||
|
|
|
@ -869,32 +869,56 @@ Zotero.Items = function() {
|
|||
|
||||
/**
|
||||
* @param {Integer} libraryID - Library to delete from
|
||||
* @param {Integer} [days] - Only delete items deleted more than this many days ago
|
||||
* @param {Integer} [limit]
|
||||
* @param {Object} [options]
|
||||
* @param {Function} [options.onProgress] - fn(progress, progressMax)
|
||||
* @param {Integer} [options.days] - Only delete items deleted more than this many days ago
|
||||
*/
|
||||
this.emptyTrash = Zotero.Promise.coroutine(function* (libraryID, days, limit) {
|
||||
this.emptyTrash = async function (libraryID, options = {}) {
|
||||
if (typeof arguments[1] == 'number') {
|
||||
Zotero.warn("Zotero.Items.emptyTrash() has changed -- update your code");
|
||||
options.days = arguments[1];
|
||||
}
|
||||
|
||||
if (!libraryID) {
|
||||
throw new Error("Library ID not provided");
|
||||
}
|
||||
|
||||
var t = new Date();
|
||||
|
||||
var deletedIDs = [];
|
||||
var deleted = await this.getDeleted(libraryID, false, options.days);
|
||||
var processed = 0;
|
||||
if (deleted.length) {
|
||||
let toDelete = {
|
||||
top: [],
|
||||
child: []
|
||||
};
|
||||
deleted.forEach((item) => {
|
||||
item.isTopLevelItem() ? toDelete.top.push(item.id) : toDelete.child.push(item.id)
|
||||
});
|
||||
|
||||
deletedIDs = yield this.getDeleted(libraryID, true, days);
|
||||
if (deletedIDs.length) {
|
||||
yield Zotero.Utilities.Internal.forEachChunkAsync(deletedIDs, 50, Zotero.Promise.coroutine(function* (chunk) {
|
||||
yield this.erase(chunk);
|
||||
yield Zotero.Notifier.trigger('refresh', 'trash', libraryID);
|
||||
}.bind(this)));
|
||||
// Show progress meter during deletions
|
||||
let eraseOptions = options.onProgress
|
||||
? {
|
||||
onProgress: function (progress, progressMax) {
|
||||
options.onProgress(processed + progress, deleted.length);
|
||||
}
|
||||
}
|
||||
: undefined;
|
||||
for (let x of ['top', 'child']) {
|
||||
await Zotero.Utilities.Internal.forEachChunkAsync(
|
||||
toDelete[x],
|
||||
1000,
|
||||
async function (chunk) {
|
||||
await this.erase(chunk, eraseOptions);
|
||||
processed += chunk.length;
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
Zotero.debug("Emptied " + deleted.length + " item(s) from trash in " + (new Date() - t) + " ms");
|
||||
}
|
||||
|
||||
if (deletedIDs.length) {
|
||||
Zotero.debug("Emptied " + deletedIDs.length + " item(s) from trash in " + (new Date() - t) + " ms");
|
||||
}
|
||||
|
||||
return deletedIDs.length;
|
||||
});
|
||||
return deleted.length;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -702,7 +702,11 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio
|
|||
yield this.refresh(skipExpandMatchParents);
|
||||
refreshed = true;
|
||||
madeChanges = true;
|
||||
sort = true;
|
||||
// Don't bother re-sorting in trash, since it's probably just a modification of a parent
|
||||
// item that's about to be deleted
|
||||
if (!collectionTreeRow.isTrash()) {
|
||||
sort = true;
|
||||
}
|
||||
}
|
||||
|
||||
else if (collectionTreeRow.isFeed()) {
|
||||
|
|
|
@ -39,41 +39,30 @@ Zotero.Sync.EventListeners.ChangeListener = new function () {
|
|||
return;
|
||||
}
|
||||
|
||||
var syncSQL = "REPLACE INTO syncDeleteLog (syncObjectTypeID, libraryID, key) "
|
||||
+ "VALUES (?, ?, ?)";
|
||||
var storageSQL = "REPLACE INTO storageDeleteLog (libraryID, key) VALUES (?, ?)";
|
||||
var syncSQL = "REPLACE INTO syncDeleteLog (syncObjectTypeID, libraryID, key) VALUES ";
|
||||
var storageSQL = "REPLACE INTO storageDeleteLog (libraryID, key) VALUES ";
|
||||
|
||||
var storageForLibrary = {};
|
||||
|
||||
return Zotero.Utilities.Internal.forEachChunkAsync(
|
||||
ids,
|
||||
100,
|
||||
function (chunk) {
|
||||
return Zotero.DB.executeTransaction(function* () {
|
||||
for (let id of chunk) {
|
||||
if (extraData[id] && extraData[id].skipDeleteLog) {
|
||||
continue;
|
||||
}
|
||||
|
||||
async function (chunk) {
|
||||
var syncSets = [];
|
||||
var storageSets = [];
|
||||
chunk
|
||||
.filter(id => !extraData[id] || !extraData[id].skipDeleteLog)
|
||||
.forEach(id => {
|
||||
if (type == 'setting') {
|
||||
var [libraryID, key] = id.split("/");
|
||||
}
|
||||
else {
|
||||
var { libraryID, key } = extraData[id];
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
throw new Error("Key not provided in notifier object");
|
||||
}
|
||||
|
||||
yield Zotero.DB.queryAsync(
|
||||
syncSQL,
|
||||
[
|
||||
syncObjectTypeID,
|
||||
libraryID,
|
||||
key
|
||||
]
|
||||
);
|
||||
syncSets.push(syncObjectTypeID, libraryID, key);
|
||||
|
||||
if (type == 'item') {
|
||||
if (storageForLibrary[libraryID] === undefined) {
|
||||
|
@ -81,17 +70,28 @@ Zotero.Sync.EventListeners.ChangeListener = new function () {
|
|||
Zotero.Sync.Storage.Local.getModeForLibrary(libraryID) == 'webdav';
|
||||
}
|
||||
if (storageForLibrary[libraryID] && extraData[id].storageDeleteLog) {
|
||||
yield Zotero.DB.queryAsync(
|
||||
storageSQL,
|
||||
[
|
||||
libraryID,
|
||||
key
|
||||
]
|
||||
);
|
||||
storageSets.push(libraryID, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (storageSets.length) {
|
||||
return Zotero.DB.executeTransaction(function* () {
|
||||
yield Zotero.DB.queryAsync(
|
||||
syncSQL + Array(syncSets.length / 3).fill('(?, ?, ?)').join(', '),
|
||||
syncSets
|
||||
);
|
||||
yield Zotero.DB.queryAsync(
|
||||
storageSQL + Array(storageSets.length / 3).fill('(?, ?)').join(', '),
|
||||
storageSets
|
||||
);
|
||||
});
|
||||
}
|
||||
else if (syncSets.length) {
|
||||
await Zotero.DB.queryAsync(
|
||||
syncSQL + Array(syncSets.length / 3).fill('(?, ?, ?)').join(', '), syncSets
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -2042,7 +2042,21 @@ var ZoteroPane = new function()
|
|||
+ Zotero.getString('general.actionCannotBeUndone')
|
||||
);
|
||||
if (result) {
|
||||
let deleted = yield Zotero.Items.emptyTrash(libraryID);
|
||||
Zotero.showZoteroPaneProgressMeter(null, true);
|
||||
try {
|
||||
let deleted = yield Zotero.Items.emptyTrash(
|
||||
libraryID,
|
||||
{
|
||||
onProgress: (progress, progressMax) => {
|
||||
var percentage = Math.round((progress / progressMax) * 100);
|
||||
Zotero.updateZoteroPaneProgressMeter(percentage);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
finally {
|
||||
Zotero.hideZoteroPaneOverlays();
|
||||
}
|
||||
yield Zotero.purgeDataObjects();
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue