Fix WebDAV file purging
Deleted files are purged at the end of every sync, without any delay. (If there's a conflict, it will be resolved before the file is deleted.) Orphaned files are deleted once every 10 days, since it's a potentially expensive operation for the server.
This commit is contained in:
parent
7fbfdce00e
commit
acb45593e7
6 changed files with 294 additions and 276 deletions
|
@ -93,21 +93,23 @@ Zotero_Preferences.Sync = {
|
||||||
var sql = "INSERT OR IGNORE INTO settings VALUES (?,?,?)";
|
var sql = "INSERT OR IGNORE INTO settings VALUES (?,?,?)";
|
||||||
Zotero.DB.query(sql, ['storage', 'zfsPurge', 'user']);
|
Zotero.DB.query(sql, ['storage', 'zfsPurge', 'user']);
|
||||||
|
|
||||||
Zotero.Sync.Storage.ZFS.purgeDeletedStorageFiles(function (success) {
|
Zotero.Sync.Storage.ZFS.purgeDeletedStorageFiles()
|
||||||
if (success) {
|
.then(function () {
|
||||||
ps.alert(
|
ps.alert(
|
||||||
null,
|
null,
|
||||||
Zotero.getString("general.success"),
|
Zotero.getString("general.success"),
|
||||||
"Attachment files from your personal library have been removed from the Zotero servers."
|
"Attachment files from your personal library have been removed from the Zotero servers."
|
||||||
);
|
);
|
||||||
}
|
})
|
||||||
else {
|
.catch(function (e) {
|
||||||
|
Zotero.debug(e, 1);
|
||||||
|
Components.utils.reportError(e);
|
||||||
|
|
||||||
ps.alert(
|
ps.alert(
|
||||||
null,
|
null,
|
||||||
Zotero.getString("general.error"),
|
Zotero.getString("general.error"),
|
||||||
"An error occurred. Please try again later."
|
"An error occurred. Please try again later."
|
||||||
);
|
);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -326,6 +326,24 @@ Zotero.Sync.Storage = new function () {
|
||||||
Zotero.debug("File sync failed for library " + libraryID);
|
Zotero.debug("File sync failed for library " + libraryID);
|
||||||
finalPromises.push([libraryID, libraryQueues]);
|
finalPromises.push([libraryID, libraryQueues]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If WebDAV sync enabled, purge deleted and orphaned files
|
||||||
|
if (libraryID == 0 && Zotero.Sync.Storage.WebDAV.includeUserFiles) {
|
||||||
|
Zotero.Sync.Storage.WebDAV.purgeDeletedStorageFiles()
|
||||||
|
.then(function () {
|
||||||
|
return Zotero.Sync.Storage.WebDAV.purgeOrphanedStorageFiles();
|
||||||
|
})
|
||||||
|
.catch(function (e) {
|
||||||
|
Zotero.debug(e, 1);
|
||||||
|
Components.utils.reportError(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Zotero.Sync.Storage.ZFS.purgeDeletedStorageFiles()
|
||||||
|
.catch(function (e) {
|
||||||
|
Zotero.debug(e, 1);
|
||||||
|
Components.utils.reportError(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (promises.length && !changedLibraries.length) {
|
if (promises.length && !changedLibraries.length) {
|
||||||
|
@ -1755,20 +1773,11 @@ Zotero.Sync.Storage = new function () {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inner
|
* @inner
|
||||||
* @param {Integer} [days=pref:e.z.sync.storage.deleteDelayDays]
|
|
||||||
* Number of days old files have to be
|
|
||||||
* @return {String[]|FALSE} Array of keys, or FALSE if none
|
* @return {String[]|FALSE} Array of keys, or FALSE if none
|
||||||
*/
|
*/
|
||||||
this.getDeletedFiles = function (days) {
|
this.getDeletedFiles = function () {
|
||||||
if (!days) {
|
var sql = "SELECT key FROM storageDeleteLog";
|
||||||
days = Zotero.Prefs.get("sync.storage.deleteDelayDays");
|
return Zotero.DB.columnQuery(sql);
|
||||||
}
|
|
||||||
|
|
||||||
var ts = Zotero.Date.getUnixTimestamp();
|
|
||||||
ts = ts - (86400 * days);
|
|
||||||
|
|
||||||
var sql = "SELECT key FROM storageDeleteLog WHERE timestamp<?";
|
|
||||||
return Zotero.DB.columnQuery(sql, ts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -473,21 +473,21 @@ Zotero.Sync.Storage.WebDAV = (function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (files.length == 0) {
|
if (files.length == 0) {
|
||||||
return Q.resolve(results);
|
return Q(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
let deleteURI = _rootURI.clone();
|
let deleteURI = _rootURI.clone();
|
||||||
// This should never happen, but let's be safe
|
// This should never happen, but let's be safe
|
||||||
if (!deleteURI.spec.match(/\/$/)) {
|
if (!deleteURI.spec.match(/\/$/)) {
|
||||||
throw new Error(
|
return Q.reject("Root URI does not end in slash in "
|
||||||
"Root URI does not end in slash in "
|
+ "Zotero.Sync.Storage.WebDAV.deleteStorageFiles()");
|
||||||
+ "Zotero.Sync.Storage.WebDAV.deleteStorageFiles()"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
results = Q.resolve(results);
|
var funcs = [];
|
||||||
files.forEach(function (fileName) {
|
for (let i=0; i<files.length; i++) {
|
||||||
results = results.then(function (results) {
|
let fileName = files[i];
|
||||||
|
let baseName = fileName.match(/^([^\.]+)/)[1];
|
||||||
|
funcs.push(function () {
|
||||||
let deleteURI = _rootURI.clone();
|
let deleteURI = _rootURI.clone();
|
||||||
deleteURI.QueryInterface(Components.interfaces.nsIURL);
|
deleteURI.QueryInterface(Components.interfaces.nsIURL);
|
||||||
deleteURI.fileName = fileName;
|
deleteURI.fileName = fileName;
|
||||||
|
@ -502,30 +502,25 @@ Zotero.Sync.Storage.WebDAV = (function () {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 404:
|
case 404:
|
||||||
var fileDeleted = false;
|
var fileDeleted = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If an item file URI, get the property URI
|
// If an item file URI, get the property URI
|
||||||
var deletePropURI = getPropertyURIFromItemURI(deleteURI);
|
var deletePropURI = getPropertyURIFromItemURI(deleteURI);
|
||||||
if (!deletePropURI) {
|
|
||||||
|
// If we already deleted the prop file, skip it
|
||||||
|
if (!deletePropURI || results.deleted.indexOf(deletePropURI.fileName) != -1) {
|
||||||
if (fileDeleted) {
|
if (fileDeleted) {
|
||||||
results.deleted.push(fileName);
|
results.deleted.push(baseName);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
results.missing.push(fileName);
|
results.missing.push(baseName);
|
||||||
}
|
}
|
||||||
return results;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If property file appears separately in delete queue,
|
let propFileName = deletePropURI.fileName;
|
||||||
// remove it, since we're taking care of it here
|
|
||||||
var propIndex = files.indexOf(deletePropURI.fileName);
|
|
||||||
if (propIndex > i) {
|
|
||||||
delete files[propIndex];
|
|
||||||
i--;
|
|
||||||
last = (i == files.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete property file
|
// Delete property file
|
||||||
return Zotero.HTTP.promise("DELETE", deletePropURI, { successCodes: [200, 204, 404] })
|
return Zotero.HTTP.promise("DELETE", deletePropURI, { successCodes: [200, 204, 404] })
|
||||||
|
@ -534,29 +529,40 @@ Zotero.Sync.Storage.WebDAV = (function () {
|
||||||
case 204:
|
case 204:
|
||||||
// IIS 5.1 and Sakai return 200
|
// IIS 5.1 and Sakai return 200
|
||||||
case 200:
|
case 200:
|
||||||
results.deleted.push(fileName);
|
results.deleted.push(baseName);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 404:
|
case 404:
|
||||||
if (fileDeleted) {
|
if (fileDeleted) {
|
||||||
results.deleted.push(fileName);
|
results.deleted.push(baseName);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
results.missing.push(fileName);
|
results.missing.push(baseName);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(function (e) {
|
.catch(function (e) {
|
||||||
results.error.push(fileName);
|
results.error.push(baseName);
|
||||||
var msg = "An error occurred attempting to delete "
|
throw e;
|
||||||
+ "'" + fileName
|
|
||||||
+ "' (" + e.status + " " + e.xmlhttp.statusText + ").";
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Components.utils.import("resource://zotero/concurrent-caller.js");
|
||||||
|
var caller = new ConcurrentCaller(4);
|
||||||
|
caller.stopOnError = true;
|
||||||
|
caller.setLogger(function (msg) {
|
||||||
|
Zotero.debug("[ConcurrentCaller] " + msg);
|
||||||
});
|
});
|
||||||
|
caller.setErrorLogger(function (msg) {
|
||||||
|
Components.utils.reportError(msg);
|
||||||
|
});
|
||||||
|
return caller.fcall(funcs)
|
||||||
|
.then(function () {
|
||||||
return results;
|
return results;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -881,7 +887,8 @@ Zotero.Sync.Storage.WebDAV = (function () {
|
||||||
deleteStorageFiles([item.key + ".prop"])
|
deleteStorageFiles([item.key + ".prop"])
|
||||||
.finally(function (results) {
|
.finally(function (results) {
|
||||||
deferred.resolve(false);
|
deferred.resolve(false);
|
||||||
});
|
})
|
||||||
|
.done();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (status != 200) {
|
else if (status != 200) {
|
||||||
|
@ -1457,21 +1464,21 @@ Zotero.Sync.Storage.WebDAV = (function () {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove files on storage server that were deleted locally more than
|
* Remove files on storage server that were deleted locally
|
||||||
* sync.storage.deleteDelayDays days ago
|
|
||||||
*
|
*
|
||||||
* @param {Function} callback Passed number of files deleted
|
* @param {Function} callback Passed number of files deleted
|
||||||
*/
|
*/
|
||||||
obj._purgeDeletedStorageFiles = function () {
|
obj._purgeDeletedStorageFiles = function () {
|
||||||
if (!this._active) {
|
return Q.fcall(function () {
|
||||||
return Q(false);
|
if (!this.includeUserFiles) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Zotero.debug("Purging deleted storage files");
|
Zotero.debug("Purging deleted storage files");
|
||||||
var files = Zotero.Sync.Storage.getDeletedFiles();
|
var files = Zotero.Sync.Storage.getDeletedFiles();
|
||||||
if (!files) {
|
if (!files) {
|
||||||
Zotero.debug("No files to delete remotely");
|
Zotero.debug("No files to delete remotely");
|
||||||
return Q(false);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add .zip extension
|
// Add .zip extension
|
||||||
|
@ -1500,20 +1507,22 @@ Zotero.Sync.Storage.WebDAV = (function () {
|
||||||
Zotero.DB.commitTransaction();
|
Zotero.DB.commitTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Zotero.debug(results);
|
||||||
|
|
||||||
return results.deleted.length;
|
return results.deleted.length;
|
||||||
});
|
});
|
||||||
|
}.bind(this));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete orphaned storage files older than a day before last sync time
|
* Delete orphaned storage files older than a day before last sync time
|
||||||
*
|
|
||||||
* @param {Function} callback
|
|
||||||
*/
|
*/
|
||||||
obj._purgeOrphanedStorageFiles = function (callback) {
|
obj._purgeOrphanedStorageFiles = function () {
|
||||||
|
return Q.fcall(function () {
|
||||||
const daysBeforeSyncTime = 1;
|
const daysBeforeSyncTime = 1;
|
||||||
|
|
||||||
if (!this._active) {
|
if (!this.includeUserFiles) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1535,25 +1544,30 @@ Zotero.Sync.Storage.WebDAV = (function () {
|
||||||
|
|
||||||
var lastSyncDate = new Date(Zotero.Sync.Server.lastLocalSyncTime * 1000);
|
var lastSyncDate = new Date(Zotero.Sync.Server.lastLocalSyncTime * 1000);
|
||||||
|
|
||||||
Zotero.HTTP.WebDAV.doProp("PROPFIND", uri, xmlstr, function (req) {
|
var deferred = Q.defer();
|
||||||
Zotero.debug(req.responseText);
|
|
||||||
|
Zotero.HTTP.WebDAV.doProp("PROPFIND", uri, xmlstr, function (xmlhttp) {
|
||||||
|
Q.fcall(function () {
|
||||||
|
Zotero.debug(xmlhttp.responseText);
|
||||||
|
|
||||||
var funcName = "Zotero.Sync.Storage.purgeOrphanedStorageFiles()";
|
var funcName = "Zotero.Sync.Storage.purgeOrphanedStorageFiles()";
|
||||||
|
|
||||||
var responseNode = req.responseXML.documentElement;
|
var responseNode = xmlhttp.responseXML.documentElement;
|
||||||
responseNode.xpath = function (path) {
|
responseNode.xpath = function (path) {
|
||||||
return Zotero.Utilities.xpath(this, path, { D: 'DAV:' });
|
return Zotero.Utilities.xpath(this, path, { D: 'DAV:' });
|
||||||
};
|
};
|
||||||
|
|
||||||
var deleteFiles = [];
|
var deleteFiles = [];
|
||||||
var trailingSlash = !!path.match(/\/$/);
|
var trailingSlash = !!path.match(/\/$/);
|
||||||
for each(var response in responseNode.xpath("response")) {
|
for each(var response in responseNode.xpath("D:response")) {
|
||||||
var href = Zotero.Utilities.xpath(response, "href", { D: 'DAV:' });
|
var href = Zotero.Utilities.xpathText(
|
||||||
href = href.length ? href[0] : ''
|
response, "D:href", { D: 'DAV:' }
|
||||||
|
) || "";
|
||||||
|
Zotero.debug(href);
|
||||||
|
|
||||||
// Strip trailing slash if there isn't one on the root path
|
// Strip trailing slash if there isn't one on the root path
|
||||||
if (!trailingSlash) {
|
if (!trailingSlash) {
|
||||||
href = href.replace(/\/$/, "")
|
href = href.replace(/\/$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Absolute
|
// Absolute
|
||||||
|
@ -1579,7 +1593,7 @@ Zotero.Sync.Storage.WebDAV = (function () {
|
||||||
// character in the URL and the server (e.g., Sakai) is
|
// character in the URL and the server (e.g., Sakai) is
|
||||||
// encoding the value
|
// encoding the value
|
||||||
&& decodeURIComponent(href).indexOf(path) == -1) {
|
&& decodeURIComponent(href).indexOf(path) == -1) {
|
||||||
Zotero.Sync.Storage.EventManager.error(
|
throw new Error(
|
||||||
"DAV:href '" + href + "' does not begin with path '"
|
"DAV:href '" + href + "' does not begin with path '"
|
||||||
+ path + "' in " + funcName
|
+ path + "' in " + funcName
|
||||||
);
|
);
|
||||||
|
@ -1587,9 +1601,9 @@ Zotero.Sync.Storage.WebDAV = (function () {
|
||||||
|
|
||||||
var matches = href.match(/[^\/]+$/);
|
var matches = href.match(/[^\/]+$/);
|
||||||
if (!matches) {
|
if (!matches) {
|
||||||
Zotero.Sync.Storage.EventManager.error(
|
throw new Error(
|
||||||
"Unexpected href '" + href + "' in " + funcName
|
"Unexpected href '" + href + "' in " + funcName
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
var file = matches[0];
|
var file = matches[0];
|
||||||
|
|
||||||
|
@ -1612,10 +1626,10 @@ Zotero.Sync.Storage.WebDAV = (function () {
|
||||||
Zotero.debug("Checking orphaned file " + file);
|
Zotero.debug("Checking orphaned file " + file);
|
||||||
|
|
||||||
// TODO: Parse HTTP date properly
|
// TODO: Parse HTTP date properly
|
||||||
var lastModified = Zotero.Utilities.xpath(
|
Zotero.debug(response.innerHTML);
|
||||||
response, "//getlastmodified", { D: 'DAV:' }
|
var lastModified = Zotero.Utilities.xpathText(
|
||||||
|
response, ".//D:getlastmodified", { D: 'DAV:' }
|
||||||
);
|
);
|
||||||
lastModified = lastModified.length ? lastModified[0] : ''
|
|
||||||
lastModified = Zotero.Date.strToISO(lastModified);
|
lastModified = Zotero.Date.strToISO(lastModified);
|
||||||
lastModified = Zotero.Date.sqlToDate(lastModified);
|
lastModified = Zotero.Date.sqlToDate(lastModified);
|
||||||
|
|
||||||
|
@ -1627,12 +1641,22 @@ Zotero.Sync.Storage.WebDAV = (function () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteStorageFiles(deleteFiles)
|
return deleteStorageFiles(deleteFiles)
|
||||||
.then(function (results) {
|
.then(function (results) {
|
||||||
Zotero.Prefs.set("lastWebDAVOrphanPurge", Math.round(new Date().getTime() / 1000))
|
Zotero.Prefs.set("lastWebDAVOrphanPurge", Math.round(new Date().getTime() / 1000))
|
||||||
Zotero.debug(results);
|
Zotero.debug(results);
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.catch(function (e) {
|
||||||
|
deferred.reject(e);
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
deferred.resolve();
|
||||||
|
});
|
||||||
}, { Depth: 1 });
|
}, { Depth: 1 });
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
}.bind(this));
|
||||||
};
|
};
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
|
|
|
@ -1006,7 +1006,7 @@ Zotero.Sync.Storage.ZFS = (function () {
|
||||||
Zotero.debug("Credentials are cached");
|
Zotero.debug("Credentials are cached");
|
||||||
_cachedCredentials = true;
|
_cachedCredentials = true;
|
||||||
})
|
})
|
||||||
.fail(function (e) {
|
.catch(function (e) {
|
||||||
if (e instanceof Zotero.HTTP.UnexpectedStatusException) {
|
if (e instanceof Zotero.HTTP.UnexpectedStatusException) {
|
||||||
if (e.status == 401) {
|
if (e.status == 401) {
|
||||||
var msg = "File sync login failed\n\n"
|
var msg = "File sync login failed\n\n"
|
||||||
|
@ -1030,7 +1030,8 @@ Zotero.Sync.Storage.ZFS = (function () {
|
||||||
/**
|
/**
|
||||||
* Remove all synced files from the server
|
* Remove all synced files from the server
|
||||||
*/
|
*/
|
||||||
obj._purgeDeletedStorageFiles = function (callback) {
|
obj._purgeDeletedStorageFiles = function () {
|
||||||
|
return Q.fcall(function () {
|
||||||
// If we don't have a user id we've never synced and don't need to bother
|
// If we don't have a user id we've never synced and don't need to bother
|
||||||
if (!Zotero.userID) {
|
if (!Zotero.userID) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -1066,21 +1067,12 @@ Zotero.Sync.Storage.ZFS = (function () {
|
||||||
}
|
}
|
||||||
uri.spec = uri.spec.substr(0, uri.spec.length - 1);
|
uri.spec = uri.spec.substr(0, uri.spec.length - 1);
|
||||||
|
|
||||||
Zotero.HTTP.doPost(uri, "", function (xmlhttp) {
|
return Zotero.HTTP.promise("POST", uri, "")
|
||||||
if (xmlhttp.status != 204) {
|
.then(function (req) {
|
||||||
if (callback) {
|
|
||||||
callback(false);
|
|
||||||
}
|
|
||||||
throw "Unexpected status code " + xmlhttp.status + " purging ZFS files";
|
|
||||||
}
|
|
||||||
|
|
||||||
var sql = "DELETE FROM settings WHERE setting=? AND key=?";
|
var sql = "DELETE FROM settings WHERE setting=? AND key=?";
|
||||||
Zotero.DB.query(sql, ['storage', 'zfsPurge']);
|
Zotero.DB.query(sql, ['storage', 'zfsPurge']);
|
||||||
|
|
||||||
if (callback) {
|
|
||||||
callback(true);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}.bind(this));
|
||||||
};
|
};
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
|
|
|
@ -429,7 +429,7 @@ Zotero.Sync.EventListener = new function () {
|
||||||
var sql = "REPLACE INTO syncDeleteLog VALUES (?, ?, ?, ?)";
|
var sql = "REPLACE INTO syncDeleteLog VALUES (?, ?, ?, ?)";
|
||||||
var syncStatement = Zotero.DB.getStatement(sql);
|
var syncStatement = Zotero.DB.getStatement(sql);
|
||||||
|
|
||||||
if (isItem && Zotero.Sync.Storage.WebDAV.active) {
|
if (isItem && Zotero.Sync.Storage.WebDAV.includeUserFiles) {
|
||||||
var storageEnabled = true;
|
var storageEnabled = true;
|
||||||
var sql = "INSERT INTO storageDeleteLog VALUES (?, ?, ?)";
|
var sql = "INSERT INTO storageDeleteLog VALUES (?, ?, ?)";
|
||||||
var storageStatement = Zotero.DB.getStatement(sql);
|
var storageStatement = Zotero.DB.getStatement(sql);
|
||||||
|
|
|
@ -1844,15 +1844,6 @@ Components.utils.import("resource://gre/modules/Services.jsm");
|
||||||
Zotero.Items.purge();
|
Zotero.Items.purge();
|
||||||
// DEBUG: this might not need to be permanent
|
// DEBUG: this might not need to be permanent
|
||||||
Zotero.Relations.purge();
|
Zotero.Relations.purge();
|
||||||
|
|
||||||
if (!skipStoragePurge && Math.random() < 1/10) {
|
|
||||||
Zotero.Sync.Storage.ZFS.purgeDeletedStorageFiles();
|
|
||||||
Zotero.Sync.Storage.WebDAV.purgeDeletedStorageFiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!skipStoragePurge) {
|
|
||||||
Zotero.Sync.Storage.WebDAV.purgeOrphanedStorageFiles();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue