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:
Dan Stillman 2013-03-26 02:28:33 -04:00
parent 7fbfdce00e
commit acb45593e7
6 changed files with 294 additions and 276 deletions

View file

@ -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."
); );
}
}); });
} }
} }

View file

@ -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);
} }

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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();
}
} }