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 = {}) {
|
Zotero.DataObject.prototype.erase = Zotero.Promise.coroutine(function* (options = {}) {
|
||||||
if (!options || typeof options != 'object') {
|
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 = {
|
var env = {
|
||||||
|
|
|
@ -909,6 +909,7 @@ Zotero.DataObjects.prototype.getPrimaryDataSQLPart = function (part) {
|
||||||
*
|
*
|
||||||
* @param {Integer|Integer[]} ids - Object ids
|
* @param {Integer|Integer[]} ids - Object ids
|
||||||
* @param {Object} [options] - See Zotero.DataObject.prototype.erase
|
* @param {Object} [options] - See Zotero.DataObject.prototype.erase
|
||||||
|
* @param {Function} [options.onProgress] - f(progress, progressMax)
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
Zotero.DataObjects.prototype.erase = Zotero.Promise.coroutine(function* (ids, options = {}) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
yield obj.erase(options);
|
yield obj.erase(options);
|
||||||
|
if (options.onProgress) {
|
||||||
|
options.onProgress(i + 1, ids.length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.unload(ids);
|
this.unload(ids);
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
|
@ -3972,13 +3972,13 @@ Zotero.Item.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
|
||||||
? (yield this.ObjectsClass.getByLibraryAndKeyAsync(this.libraryID, parentItem))
|
? (yield this.ObjectsClass.getByLibraryAndKeyAsync(this.libraryID, parentItem))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (parentItem) {
|
if (parentItem && !env.options.skipParentRefresh) {
|
||||||
Zotero.Notifier.queue('refresh', 'item', parentItem.id);
|
Zotero.Notifier.queue('refresh', 'item', parentItem.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Delete associated attachment files
|
// // Delete associated attachment files
|
||||||
if (this.isAttachment()) {
|
if (this.isAttachment()) {
|
||||||
let linkMode = this.getAttachmentLinkMode();
|
let linkMode = this.attachmentLinkMode;
|
||||||
// If link only, nothing to delete
|
// If link only, nothing to delete
|
||||||
if (linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
if (linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
||||||
try {
|
try {
|
||||||
|
@ -4005,7 +4005,9 @@ Zotero.Item.prototype._eraseData = Zotero.Promise.coroutine(function* (env) {
|
||||||
for (let i=0; i<toDelete.length; i++) {
|
for (let i=0; i<toDelete.length; i++) {
|
||||||
let obj = yield this.ObjectsClass.getAsync(toDelete[i]);
|
let obj = yield this.ObjectsClass.getAsync(toDelete[i]);
|
||||||
// Copy all options other than 'tx', which would cause a deadlock
|
// Copy all options other than 'tx', which would cause a deadlock
|
||||||
let options = {};
|
let options = {
|
||||||
|
skipParentRefresh: true
|
||||||
|
};
|
||||||
Object.assign(options, env.options);
|
Object.assign(options, env.options);
|
||||||
delete options.tx;
|
delete options.tx;
|
||||||
yield obj.erase(options);
|
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);
|
yield Zotero.DB.queryAsync('DELETE FROM items WHERE itemID=?', this.id);
|
||||||
|
|
||||||
if (parentItem) {
|
if (parentItem && !env.options.skipParentRefresh) {
|
||||||
yield parentItem.reload(['primaryData', 'childItems'], true);
|
yield parentItem.reload(['primaryData', 'childItems'], true);
|
||||||
parentItem.clearBestAttachmentState();
|
parentItem.clearBestAttachmentState();
|
||||||
}
|
}
|
||||||
|
|
|
@ -869,32 +869,56 @@ Zotero.Items = function() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Integer} libraryID - Library to delete from
|
* @param {Integer} libraryID - Library to delete from
|
||||||
* @param {Integer} [days] - Only delete items deleted more than this many days ago
|
* @param {Object} [options]
|
||||||
* @param {Integer} [limit]
|
* @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) {
|
if (!libraryID) {
|
||||||
throw new Error("Library ID not provided");
|
throw new Error("Library ID not provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
var t = new Date();
|
var t = new Date();
|
||||||
|
|
||||||
var deletedIDs = [];
|
var deleted = await this.getDeleted(libraryID, false, options.days);
|
||||||
|
var processed = 0;
|
||||||
deletedIDs = yield this.getDeleted(libraryID, true, days);
|
if (deleted.length) {
|
||||||
if (deletedIDs.length) {
|
let toDelete = {
|
||||||
yield Zotero.Utilities.Internal.forEachChunkAsync(deletedIDs, 50, Zotero.Promise.coroutine(function* (chunk) {
|
top: [],
|
||||||
yield this.erase(chunk);
|
child: []
|
||||||
yield Zotero.Notifier.trigger('refresh', 'trash', libraryID);
|
};
|
||||||
}.bind(this)));
|
deleted.forEach((item) => {
|
||||||
|
item.isTopLevelItem() ? toDelete.top.push(item.id) : toDelete.child.push(item.id)
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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) {
|
return deleted.length;
|
||||||
Zotero.debug("Emptied " + deletedIDs.length + " item(s) from trash in " + (new Date() - t) + " ms");
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return deletedIDs.length;
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -702,7 +702,11 @@ Zotero.ItemTreeView.prototype.notify = Zotero.Promise.coroutine(function* (actio
|
||||||
yield this.refresh(skipExpandMatchParents);
|
yield this.refresh(skipExpandMatchParents);
|
||||||
refreshed = true;
|
refreshed = true;
|
||||||
madeChanges = 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()) {
|
else if (collectionTreeRow.isFeed()) {
|
||||||
|
|
|
@ -39,41 +39,30 @@ Zotero.Sync.EventListeners.ChangeListener = new function () {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var syncSQL = "REPLACE INTO syncDeleteLog (syncObjectTypeID, libraryID, key) "
|
var syncSQL = "REPLACE INTO syncDeleteLog (syncObjectTypeID, libraryID, key) VALUES ";
|
||||||
+ "VALUES (?, ?, ?)";
|
var storageSQL = "REPLACE INTO storageDeleteLog (libraryID, key) VALUES ";
|
||||||
var storageSQL = "REPLACE INTO storageDeleteLog (libraryID, key) VALUES (?, ?)";
|
|
||||||
|
|
||||||
var storageForLibrary = {};
|
var storageForLibrary = {};
|
||||||
|
|
||||||
return Zotero.Utilities.Internal.forEachChunkAsync(
|
return Zotero.Utilities.Internal.forEachChunkAsync(
|
||||||
ids,
|
ids,
|
||||||
100,
|
100,
|
||||||
function (chunk) {
|
async function (chunk) {
|
||||||
return Zotero.DB.executeTransaction(function* () {
|
var syncSets = [];
|
||||||
for (let id of chunk) {
|
var storageSets = [];
|
||||||
if (extraData[id] && extraData[id].skipDeleteLog) {
|
chunk
|
||||||
continue;
|
.filter(id => !extraData[id] || !extraData[id].skipDeleteLog)
|
||||||
}
|
.forEach(id => {
|
||||||
|
|
||||||
if (type == 'setting') {
|
if (type == 'setting') {
|
||||||
var [libraryID, key] = id.split("/");
|
var [libraryID, key] = id.split("/");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var { libraryID, key } = extraData[id];
|
var { libraryID, key } = extraData[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!key) {
|
if (!key) {
|
||||||
throw new Error("Key not provided in notifier object");
|
throw new Error("Key not provided in notifier object");
|
||||||
}
|
}
|
||||||
|
syncSets.push(syncObjectTypeID, libraryID, key);
|
||||||
yield Zotero.DB.queryAsync(
|
|
||||||
syncSQL,
|
|
||||||
[
|
|
||||||
syncObjectTypeID,
|
|
||||||
libraryID,
|
|
||||||
key
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (type == 'item') {
|
if (type == 'item') {
|
||||||
if (storageForLibrary[libraryID] === undefined) {
|
if (storageForLibrary[libraryID] === undefined) {
|
||||||
|
@ -81,17 +70,28 @@ Zotero.Sync.EventListeners.ChangeListener = new function () {
|
||||||
Zotero.Sync.Storage.Local.getModeForLibrary(libraryID) == 'webdav';
|
Zotero.Sync.Storage.Local.getModeForLibrary(libraryID) == 'webdav';
|
||||||
}
|
}
|
||||||
if (storageForLibrary[libraryID] && extraData[id].storageDeleteLog) {
|
if (storageForLibrary[libraryID] && extraData[id].storageDeleteLog) {
|
||||||
yield Zotero.DB.queryAsync(
|
storageSets.push(libraryID, key);
|
||||||
storageSQL,
|
|
||||||
[
|
|
||||||
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')
|
+ Zotero.getString('general.actionCannotBeUndone')
|
||||||
);
|
);
|
||||||
if (result) {
|
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();
|
yield Zotero.purgeDataObjects();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue