Use multi-item requests for full-text writes

This is necessary to get a library version after the write instead of an
item version. Otherwise after a full-text write, the main library
version is behind, so the next sync checks all object types for that
library instead of getting a 304.

Full text is batched up to 500K characters or 10 items, whichever is
less.

This also switches to using ?format=versions for /fulltext requests,
which isn't currently necessary but reflects what it's actually doing.
This commit is contained in:
Dan Stillman 2016-05-02 13:13:19 -04:00
parent a0c7cf9bee
commit e0e744f9b1
6 changed files with 246 additions and 122 deletions

View file

@ -105,6 +105,7 @@ Zotero.Fulltext = Zotero.FullText = new function(){
this.getLibraryVersion = function (libraryID) { this.getLibraryVersion = function (libraryID) {
if (!libraryID) throw new Error("libraryID not provided");
return Zotero.DB.valueQueryAsync( return Zotero.DB.valueQueryAsync(
"SELECT version FROM version WHERE schema=?", "fulltext_" + libraryID "SELECT version FROM version WHERE schema=?", "fulltext_" + libraryID
) )
@ -112,6 +113,7 @@ Zotero.Fulltext = Zotero.FullText = new function(){
this.setLibraryVersion = Zotero.Promise.coroutine(function* (libraryID, version) { this.setLibraryVersion = Zotero.Promise.coroutine(function* (libraryID, version) {
if (!libraryID) throw new Error("libraryID not provided");
yield Zotero.DB.queryAsync( yield Zotero.DB.queryAsync(
"REPLACE INTO version VALUES (?, ?)", ["fulltext_" + libraryID, version] "REPLACE INTO version VALUES (?, ?)", ["fulltext_" + libraryID, version]
); );
@ -130,12 +132,12 @@ Zotero.Fulltext = Zotero.FullText = new function(){
}); });
this.setItemSynced = Zotero.Promise.coroutine(function* (itemID, version) { this.setItemSynced = function (itemID, version) {
return Zotero.DB.queryAsync( return Zotero.DB.queryAsync(
"UPDATE fulltextItems SET synced=?, version=? WHERE itemID=?", "UPDATE fulltextItems SET synced=?, version=? WHERE itemID=?",
[SYNC_STATE_IN_SYNC, version, itemID] [SYNC_STATE_IN_SYNC, version, itemID]
); );
}); };
// this is a port from http://mxr.mozilla.org/mozilla-central/source/intl/lwbrk/src/nsSampleWordBreaker.cpp to // this is a port from http://mxr.mozilla.org/mozilla-central/source/intl/lwbrk/src/nsSampleWordBreaker.cpp to
@ -787,22 +789,25 @@ Zotero.Fulltext = Zotero.FullText = new function(){
* Get content and stats that haven't yet been synced * Get content and stats that haven't yet been synced
* *
* @param {Integer} libraryID * @param {Integer} libraryID
* @param {Integer} numItems * @param {Integer} [options]
* @param {Integer} [options.maxSize]
* @param {Integer} [options.maxItems]
* @param {Integer} [options.lastItemID] - Only return content for items above this id
* @return {Promise<Array<Object>>} * @return {Promise<Array<Object>>}
*/ */
this.getUnsyncedContent = Zotero.Promise.coroutine(function* (libraryID, numItems) { this.getUnsyncedContent = Zotero.Promise.coroutine(function* (libraryID, options = {}) {
var maxLength = Zotero.Prefs.get('fulltext.textMaxLength');
var contentItems = []; var contentItems = [];
var sql = "SELECT itemID, indexedChars, totalChars, indexedPages, totalPages " var sql = "SELECT itemID, indexedChars, totalChars, indexedPages, totalPages "
+ "FROM fulltextItems FI JOIN items I USING (itemID) WHERE libraryID=? AND " + "FROM fulltextItems FI JOIN items I USING (itemID) WHERE libraryID=? AND "
+ "FI.synced=? AND I.synced=1 ORDER BY clientDateModified DESC"; + "FI.synced=? AND I.synced=1 ";
var params = [libraryID, SYNC_STATE_UNSYNCED]; var params = [libraryID, SYNC_STATE_UNSYNCED];
if (numItems) { if (options.lastItemID) {
sql += " LIMIT ?"; sql += "AND itemID>?";
params.push(numItems); params.push(options.lastItemID);
} }
sql += "ORDER BY itemID DESC";
var rows = yield Zotero.DB.queryAsync(sql, params); var rows = yield Zotero.DB.queryAsync(sql, params);
var contentSize = 0;
for (let i = 0; i < rows.length; i++) { for (let i = 0; i < rows.length; i++) {
let row = rows[i]; let row = rows[i];
let content; let content;
@ -868,8 +873,13 @@ Zotero.Fulltext = Zotero.FullText = new function(){
continue; continue;
} }
// If this isn't the first item and it would put us over the size limit, stop
if (contentItems.length && options.maxSize && contentSize + content.length > options.maxSize) {
break;
}
contentItems.push({ contentItems.push({
libraryID: item.libraryID, itemID: item.id,
key: item.key, key: item.key,
content, content,
indexedChars: row.indexedChars ? row.indexedChars : 0, indexedChars: row.indexedChars ? row.indexedChars : 0,
@ -877,6 +887,11 @@ Zotero.Fulltext = Zotero.FullText = new function(){
indexedPages: row.indexedPages ? row.indexedPages : 0, indexedPages: row.indexedPages ? row.indexedPages : 0,
totalPages: row.totalPages ? row.totalPages : 0 totalPages: row.totalPages ? row.totalPages : 0
}); });
if (options.maxItems && contentItems.length >= options.maxItems) {
break;
}
contentSize += content.length;
} }
return contentItems; return contentItems;
}); });

View file

@ -376,7 +376,8 @@ Zotero.Sync.APIClient.prototype = {
var params = { var params = {
libraryType: libraryType, libraryType: libraryType,
libraryTypeID: libraryTypeID, libraryTypeID: libraryTypeID,
target: "fulltext" target: "fulltext",
format: "versions"
}; };
if (since) { if (since) {
params.since = since; params.since = since;
@ -415,26 +416,31 @@ Zotero.Sync.APIClient.prototype = {
}), }),
setFullTextForItem: Zotero.Promise.coroutine(function* (libraryType, libraryTypeID, itemKey, data) { setFullTextForItems: Zotero.Promise.coroutine(function* (libraryType, libraryTypeID, libraryVersion, data) {
var params = { var params = {
libraryType: libraryType, libraryType: libraryType,
libraryTypeID: libraryTypeID, libraryTypeID: libraryTypeID,
target: `items/${itemKey}/fulltext` target: "fulltext"
}; };
var uri = this.buildRequestURI(params); var uri = this.buildRequestURI(params);
var xmlhttp = yield this.makeRequest( var xmlhttp = yield this.makeRequest(
"PUT", "POST",
uri, uri,
{ {
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json",
"If-Unmodified-Since-Version": libraryVersion
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
successCodes: [204], successCodes: [200, 412],
debug: true debug: true
} }
); );
return this._getLastModifiedVersion(xmlhttp); this._check412(xmlhttp);
return {
libraryVersion: this._getLastModifiedVersion(xmlhttp),
results: this._parseJSON(xmlhttp.responseText)
};
}), }),
@ -727,5 +733,6 @@ Zotero.Sync.APIClient.prototype = {
if (!libraryVersion) { if (!libraryVersion) {
throw new Error("Last-Modified-Version not provided"); throw new Error("Last-Modified-Version not provided");
} }
return libraryVersion;
} }
} }

View file

@ -35,6 +35,9 @@ Zotero.Sync.Data.FullTextEngine = function (options) {
throw new Error("options.libraryID not set"); throw new Error("options.libraryID not set");
} }
this.MAX_BATCH_SIZE = 500000;
this.MAX_BATCH_ITEMS = 10;
this.apiClient = options.apiClient; this.apiClient = options.apiClient;
this.libraryID = options.libraryID; this.libraryID = options.libraryID;
this.library = Zotero.Libraries.get(options.libraryID); this.library = Zotero.Libraries.get(options.libraryID);
@ -111,33 +114,71 @@ Zotero.Sync.Data.FullTextEngine.prototype._upload = Zotero.Promise.coroutine(fun
Zotero.debug("Uploading full-text content for " + this.library.name); Zotero.debug("Uploading full-text content for " + this.library.name);
var props = ['content', 'indexedChars', 'totalChars', 'indexedPages', 'totalPages']; var libraryVersion = this.library.libraryVersion;
var props = ['key', 'content', 'indexedChars', 'totalChars', 'indexedPages', 'totalPages'];
let lastItemID = 0;
while (true) { while (true) {
let numSuccessful = 0; let objs = yield Zotero.FullText.getUnsyncedContent(this.libraryID, {
let objs = yield Zotero.FullText.getUnsyncedContent(this.libraryID, 10); maxSize: this.MAX_BATCH_SIZE,
maxItems: this.MAX_BATCH_ITEMS,
lastItemID
});
if (!objs.length) { if (!objs.length) {
break; break;
} }
let promises = [];
let jsonArray = [];
let results;
for (let obj of objs) { for (let obj of objs) {
let json = {}; let json = {};
for (let prop of props) { for (let prop of props) {
json[prop] = obj[prop]; json[prop] = obj[prop];
} }
promises.push(this.apiClient.setFullTextForItem( jsonArray.push(json);
this.library.libraryType, this.library.libraryTypeID, obj.key, json lastItemID = obj.itemID;
}
({ libraryVersion, results } = yield this.apiClient.setFullTextForItems(
this.library.libraryType,
this.library.libraryTypeID,
libraryVersion,
jsonArray
)); ));
}
var results = yield Zotero.Promise.all(promises);
yield Zotero.DB.executeTransaction(function* () { yield Zotero.DB.executeTransaction(function* () {
for (let i = 0; i < results.length; i++) { for (let state of ['successful', 'unchanged']) {
let itemID = yield Zotero.Items.getIDFromLibraryAndKey( for (let index in results[state]) {
this.libraryID, objs[i].key let key = results[state][index].key;
); let itemID = Zotero.Items.getIDFromLibraryAndKey(this.libraryID, key);
yield Zotero.FullText.setItemSynced(itemID, results[i]); yield Zotero.FullText.setItemSynced(itemID, libraryVersion);
} }
}
// Set both the library version and the full-text library version. The latter is necessary
// because full-text sync can be turned off at any time, so we have to keep track of the
// last version we've seen for full-text in case the main library version has advanced since.
yield Zotero.FullText.setLibraryVersion(this.libraryID, libraryVersion);
this.library.libraryVersion = libraryVersion;
yield this.library.save();
}.bind(this)); }.bind(this));
for (let index in results.failed) {
let { code, message, data } = results.failed[index];
let e = new Error(message);
e.name = "ZoteroObjectUploadError";
e.code = code;
if (data) {
e.data = data;
}
Zotero.logError("Error uploading full text for item " + jsonArray[index].key + " in "
+ this.library.name + ":\n\n" + e);
if (this.onError) {
this.onError(e);
}
if (this.stopOnError) {
throw new Error(e);
}
}
} }
}); });

View file

@ -184,27 +184,32 @@ Zotero.Sync.Runner_Module = function (options = {}) {
// Sync data and files, and then repeat if necessary // Sync data and files, and then repeat if necessary
let attempt = 1; let attempt = 1;
let nextLibraries = librariesToSync.concat(); let successfulLibraries = new Set(librariesToSync);
let resyncLibraries = []; while (librariesToSync.length) {
while (nextLibraries.length) {
if (attempt > 3) { if (attempt > 3) {
// TODO: Back off and/or nicer error
throw new Error("Too many sync attempts -- stopping"); throw new Error("Too many sync attempts -- stopping");
} }
nextLibraries = yield _doDataSync( let nextLibraries = yield _doDataSync(librariesToSync, engineOptions);
resyncLibraries.length ? resyncLibraries : nextLibraries, // Remove failed libraries from the successful set
engineOptions Zotero.Utilities.arrayDiff(librariesToSync, nextLibraries).forEach(libraryID => {
); successfulLibraries.delete(libraryID);
resyncLibraries = yield _doFileSync(nextLibraries, engineOptions); });
if (!resyncLibraries.length) {
break; // Run file sync on all libraries that passed the last data sync
} librariesToSync = yield _doFileSync(nextLibraries, engineOptions);
if (librariesToSync.length) {
attempt++; attempt++;
continue;
} }
// Sync full-text content in libraries with successful data sync. Full-text syncing // Run full-text sync on all libraries that haven't failed a data sync
// still happens for libraries with failed file syncs. librariesToSync = yield _doFullTextSync([...successfulLibraries], engineOptions);
if (nextLibraries.length) { if (librariesToSync.length) {
yield _doFullTextSync(nextLibraries, engineOptions); attempt++;
continue;
}
break;
} }
} }
catch (e) { catch (e) {
@ -570,15 +575,22 @@ Zotero.Sync.Runner_Module = function (options = {}) {
} }
} }
Zotero.debug("Done with file syncing"); Zotero.debug("Done with file syncing");
if (resyncLibraries.length) {
Zotero.debug("Libraries to resync: " + resyncLibraries.join(", "));
}
return resyncLibraries; return resyncLibraries;
}.bind(this)); }.bind(this));
/**
* @return {Integer[]} - Array of libraries that need data syncing again
*/
var _doFullTextSync = Zotero.Promise.coroutine(function* (libraries, options) { var _doFullTextSync = Zotero.Promise.coroutine(function* (libraries, options) {
if (!Zotero.Prefs.get("sync.fulltext.enabled")) return; if (!Zotero.Prefs.get("sync.fulltext.enabled")) return;
Zotero.debug("Starting full-text syncing"); Zotero.debug("Starting full-text syncing");
this.setSyncStatus(Zotero.getString('sync.status.syncingFullText')); this.setSyncStatus(Zotero.getString('sync.status.syncingFullText'));
var resyncLibraries = [];
for (let libraryID of libraries) { for (let libraryID of libraries) {
try { try {
let opts = {}; let opts = {};
@ -589,6 +601,10 @@ Zotero.Sync.Runner_Module = function (options = {}) {
yield engine.start(); yield engine.start();
} }
catch (e) { catch (e) {
if (e instanceof Zotero.HTTP.UnexpectedStatusException && e.status == 412) {
resyncLibraries.push(libraryID);
continue;
}
Zotero.debug("Full-text sync failed for library " + libraryID); Zotero.debug("Full-text sync failed for library " + libraryID);
Zotero.logError(e); Zotero.logError(e);
this.checkError(e); this.checkError(e);
@ -600,6 +616,10 @@ Zotero.Sync.Runner_Module = function (options = {}) {
} }
} }
Zotero.debug("Done with full-text syncing"); Zotero.debug("Done with full-text syncing");
if (resyncLibraries.length) {
Zotero.debug("Libraries to resync: " + resyncLibraries.join(", "));
}
return resyncLibraries;
}.bind(this)); }.bind(this));

View file

@ -74,13 +74,13 @@ describe("Zotero.Sync.Data.FullTextEngine", function () {
var spy = sinon.spy(Zotero.Fulltext, "startContentProcessor") var spy = sinon.spy(Zotero.Fulltext, "startContentProcessor")
var itemFullTextVersion = 10; var itemFullTextVersion = 10;
var libraryFullTextVersion = 15; var libraryVersion = 15;
setResponse({ setResponse({
method: "GET", method: "GET",
url: "users/1/fulltext", url: "users/1/fulltext?format=versions",
status: 200, status: 200,
headers: { headers: {
"Last-Modified-Version": libraryFullTextVersion "Last-Modified-Version": libraryVersion
}, },
json: { json: {
[attachment.key]: itemFullTextVersion [attachment.key]: itemFullTextVersion
@ -111,7 +111,7 @@ describe("Zotero.Sync.Data.FullTextEngine", function () {
assert.propertyVal(data, 'version', itemFullTextVersion); assert.propertyVal(data, 'version', itemFullTextVersion);
yield assert.eventually.equal( yield assert.eventually.equal(
Zotero.FullText.getLibraryVersion(item.libraryID), Zotero.FullText.getLibraryVersion(item.libraryID),
libraryFullTextVersion libraryVersion
); );
sinon.assert.calledOnce(spy); sinon.assert.calledOnce(spy);
@ -134,14 +134,14 @@ describe("Zotero.Sync.Data.FullTextEngine", function () {
spy = sinon.spy(Zotero.Fulltext, "startContentProcessor") spy = sinon.spy(Zotero.Fulltext, "startContentProcessor")
itemFullTextVersion = 17; itemFullTextVersion = 17;
var lastLibraryFullTextVersion = libraryFullTextVersion; var lastLibraryVersion = libraryVersion;
libraryFullTextVersion = 20; libraryVersion = 20;
setResponse({ setResponse({
method: "GET", method: "GET",
url: "users/1/fulltext?since=" + lastLibraryFullTextVersion, url: "users/1/fulltext?format=versions&since=" + lastLibraryVersion,
status: 200, status: 200,
headers: { headers: {
"Last-Modified-Version": libraryFullTextVersion "Last-Modified-Version": libraryVersion
}, },
json: { json: {
[attachment.key]: itemFullTextVersion [attachment.key]: itemFullTextVersion
@ -172,7 +172,7 @@ describe("Zotero.Sync.Data.FullTextEngine", function () {
assert.propertyVal(data, 'version', itemFullTextVersion); assert.propertyVal(data, 'version', itemFullTextVersion);
yield assert.eventually.equal( yield assert.eventually.equal(
Zotero.FullText.getLibraryVersion(item.libraryID), Zotero.FullText.getLibraryVersion(item.libraryID),
libraryFullTextVersion libraryVersion
); );
sinon.assert.calledOnce(spy); sinon.assert.calledOnce(spy);
@ -191,13 +191,13 @@ describe("Zotero.Sync.Data.FullTextEngine", function () {
yield attachment.saveTx(); yield attachment.saveTx();
var itemFullTextVersion = 10; var itemFullTextVersion = 10;
var libraryFullTextVersion = 15; var libraryVersion = 15;
setResponse({ setResponse({
method: "GET", method: "GET",
url: "users/1/fulltext", url: "users/1/fulltext?format=versions",
status: 200, status: 200,
headers: { headers: {
"Last-Modified-Version": libraryFullTextVersion "Last-Modified-Version": libraryVersion
}, },
json: { json: {
[attachment.key]: itemFullTextVersion [attachment.key]: itemFullTextVersion
@ -225,29 +225,41 @@ describe("Zotero.Sync.Data.FullTextEngine", function () {
({ engine, client, caller } = yield setup()); ({ engine, client, caller } = yield setup());
var item = yield createDataObject('item'); var item = yield createDataObject('item');
var attachment = new Zotero.Item('attachment');
attachment.parentItemID = item.id;
attachment.attachmentLinkMode = 'imported_file';
attachment.attachmentContentType = 'text/html';
attachment.attachmentFilename = 'test.html';
attachment.attachmentCharset = 'utf-8';
attachment.synced = true;
yield attachment.saveTx();
yield Zotero.Attachments.createDirectoryForItem(attachment);
var path = attachment.getFilePath(); var attachment1 = new Zotero.Item('attachment');
var content = generateContent() attachment1.parentItemID = item.id;
var htmlContent = "<html><body>" + content + "</body></html>"; attachment1.attachmentLinkMode = 'imported_file';
yield Zotero.File.putContentsAsync(path, content); attachment1.attachmentContentType = 'text/html';
yield Zotero.Fulltext.indexItems([attachment.id]); attachment1.attachmentFilename = 'test.html';
attachment1.attachmentCharset = 'utf-8';
attachment1.synced = true;
yield attachment1.saveTx();
yield Zotero.Attachments.createDirectoryForItem(attachment1);
var path = attachment1.getFilePath();
var content1 = "A" + generateContent()
yield Zotero.File.putContentsAsync(path, content1);
var attachment2 = new Zotero.Item('attachment');
attachment2.parentItemID = item.id;
attachment2.attachmentLinkMode = 'imported_file';
attachment2.attachmentContentType = 'text/html';
attachment2.attachmentFilename = 'test.html';
attachment2.attachmentCharset = 'utf-8';
attachment2.synced = true;
yield attachment2.saveTx();
yield Zotero.Attachments.createDirectoryForItem(attachment2);
path = attachment2.getFilePath();
var content2 = "B" + generateContent()
yield Zotero.File.putContentsAsync(path, content2);
yield Zotero.Fulltext.indexItems([attachment1.id, attachment2.id]);
var libraryVersion = 15; var libraryVersion = 15;
var previousLibraryVersion = libraryVersion;
var count = 1; var count = 1;
setResponse({ setResponse({
method: "GET", method: "GET",
url: "users/1/fulltext", url: "users/1/fulltext?format=versions",
status: 200, status: 200,
headers: { headers: {
"Last-Modified-Version": libraryVersion "Last-Modified-Version": libraryVersion
@ -255,8 +267,8 @@ describe("Zotero.Sync.Data.FullTextEngine", function () {
json: {} json: {}
}); });
server.respond(function (req) { server.respond(function (req) {
if (req.method == "PUT") { if (req.method == "POST") {
if (req.url == `${baseURL}users/1/items/${attachment.key}/fulltext`) { if (req.url == `${baseURL}users/1/fulltext`) {
assert.propertyVal( assert.propertyVal(
req.requestHeaders, req.requestHeaders,
'Content-Type', 'Content-Type',
@ -264,19 +276,40 @@ describe("Zotero.Sync.Data.FullTextEngine", function () {
); );
let json = JSON.parse(req.requestBody); let json = JSON.parse(req.requestBody);
assert.propertyVal(json, 'content', content); assert.lengthOf(json, 2);
assert.propertyVal(json, 'indexedChars', content.length);
assert.propertyVal(json, 'totalChars', content.length); json.sort((a, b) => a.content < b.content ? -1 : 1);
assert.propertyVal(json, 'indexedPages', 0); assert.propertyVal(json[0], 'key', attachment1.key);
assert.propertyVal(json, 'totalPages', 0); assert.propertyVal(json[0], 'content', content1);
assert.propertyVal(json[0], 'indexedChars', content1.length);
assert.propertyVal(json[0], 'totalChars', content1.length);
assert.propertyVal(json[0], 'indexedPages', 0);
assert.propertyVal(json[0], 'totalPages', 0);
assert.propertyVal(json[1], 'key', attachment2.key);
assert.propertyVal(json[1], 'content', content2);
assert.propertyVal(json[1], 'indexedChars', content2.length);
assert.propertyVal(json[1], 'totalChars', content2.length);
assert.propertyVal(json[1], 'indexedPages', 0);
assert.propertyVal(json[1], 'totalPages', 0);
req.respond( req.respond(
204, 200,
{ {
"Content-Type": "application/json", "Content-Type": "application/json",
"Last-Modified-Version": ++libraryVersion "Last-Modified-Version": ++libraryVersion
}, },
"" JSON.stringify({
"successful": {
"0": {
key: attachment1.key
},
"1": {
key: attachment2.key
}
},
"unchanged": {},
"failed": {}
})
); );
count--; count--;
} }
@ -285,10 +318,10 @@ describe("Zotero.Sync.Data.FullTextEngine", function () {
yield engine.start(); yield engine.start();
assert.equal(count, 0); assert.equal(count, 0);
yield assert.eventually.equal( yield assert.eventually.equal(Zotero.FullText.getItemVersion(attachment1.id), libraryVersion);
Zotero.FullText.getItemVersion(attachment.id), yield assert.eventually.equal(Zotero.FullText.getItemVersion(attachment2.id), libraryVersion);
libraryVersion yield assert.eventually.equal(Zotero.Fulltext.getLibraryVersion(libraryID), libraryVersion);
); assert.equal(Zotero.Libraries.userLibrary.libraryVersion, libraryVersion);
// //
// Upload new content // Upload new content
@ -296,27 +329,25 @@ describe("Zotero.Sync.Data.FullTextEngine", function () {
({ engine, client, caller } = yield setup()); ({ engine, client, caller } = yield setup());
yield Zotero.Libraries.setVersion(libraryID, libraryVersion); yield Zotero.Libraries.setVersion(libraryID, libraryVersion);
item = yield createDataObject('item'); var attachment3 = new Zotero.Item('attachment');
attachment = new Zotero.Item('attachment'); attachment3.parentItemID = item.id;
attachment.parentItemID = item.id; attachment3.attachmentLinkMode = 'imported_file';
attachment.attachmentLinkMode = 'imported_file'; attachment3.attachmentContentType = 'text/html';
attachment.attachmentContentType = 'text/html'; attachment3.attachmentFilename = 'test.html';
attachment.attachmentFilename = 'test.html'; attachment3.attachmentCharset = 'utf-8';
attachment.attachmentCharset = 'utf-8'; attachment3.synced = true;
attachment.synced = true; yield attachment3.saveTx();
yield attachment.saveTx(); yield Zotero.Attachments.createDirectoryForItem(attachment3);
yield Zotero.Attachments.createDirectoryForItem(attachment);
path = attachment.getFilePath(); path = attachment3.getFilePath();
content = generateContent() var content3 = generateContent()
htmlContent = "<html><body>" + content + "</body></html>"; yield Zotero.File.putContentsAsync(path, content3);
yield Zotero.File.putContentsAsync(path, content); yield Zotero.Fulltext.indexItems([attachment3.id]);
yield Zotero.Fulltext.indexItems([attachment.id]);
count = 1; count = 1;
setResponse({ setResponse({
method: "GET", method: "GET",
url: "users/1/fulltext?since=" + previousLibraryVersion, url: "users/1/fulltext?format=versions&since=" + libraryVersion,
status: 200, status: 200,
headers: { headers: {
"Last-Modified-Version": libraryVersion "Last-Modified-Version": libraryVersion
@ -324,8 +355,8 @@ describe("Zotero.Sync.Data.FullTextEngine", function () {
json: {} json: {}
}); });
server.respond(function (req) { server.respond(function (req) {
if (req.method == "PUT") { if (req.method == "POST") {
if (req.url == `${baseURL}users/1/items/${attachment.key}/fulltext`) { if (req.url == `${baseURL}users/1/fulltext`) {
assert.propertyVal(req.requestHeaders, 'Zotero-API-Key', apiKey); assert.propertyVal(req.requestHeaders, 'Zotero-API-Key', apiKey);
assert.propertyVal( assert.propertyVal(
req.requestHeaders, req.requestHeaders,
@ -334,19 +365,30 @@ describe("Zotero.Sync.Data.FullTextEngine", function () {
); );
let json = JSON.parse(req.requestBody); let json = JSON.parse(req.requestBody);
assert.propertyVal(json, 'content', content); assert.lengthOf(json, 1);
assert.propertyVal(json, 'indexedChars', content.length); json = json[0];
assert.propertyVal(json, 'totalChars', content.length); assert.propertyVal(json, 'key', attachment3.key);
assert.propertyVal(json, 'content', content3);
assert.propertyVal(json, 'indexedChars', content3.length);
assert.propertyVal(json, 'totalChars', content3.length);
assert.propertyVal(json, 'indexedPages', 0); assert.propertyVal(json, 'indexedPages', 0);
assert.propertyVal(json, 'totalPages', 0); assert.propertyVal(json, 'totalPages', 0);
req.respond( req.respond(
204, 200,
{ {
"Content-Type": "application/json", "Content-Type": "application/json",
"Last-Modified-Version": ++libraryVersion "Last-Modified-Version": ++libraryVersion
}, },
"" JSON.stringify({
"successful": {
"0": {
key: attachment3.key
}
},
"unchanged": {},
"failed": {}
})
); );
count--; count--;
} }
@ -355,10 +397,9 @@ describe("Zotero.Sync.Data.FullTextEngine", function () {
yield engine.start(); yield engine.start();
assert.equal(count, 0); assert.equal(count, 0);
yield assert.eventually.equal( yield assert.eventually.equal(Zotero.FullText.getItemVersion(attachment3.id), libraryVersion);
Zotero.FullText.getItemVersion(attachment.id), yield assert.eventually.equal(Zotero.Fulltext.getLibraryVersion(libraryID), libraryVersion);
libraryVersion assert.equal(Zotero.Libraries.userLibrary.libraryVersion, libraryVersion);
);
})
}) })
});
}) })

View file

@ -608,7 +608,7 @@ describe("Zotero.Sync.Runner", function () {
// Full-text syncing // Full-text syncing
setResponse({ setResponse({
method: "GET", method: "GET",
url: "users/1/fulltext", url: "users/1/fulltext?format=versions",
status: 200, status: 200,
headers: { headers: {
"Last-Modified-Version": 5 "Last-Modified-Version": 5
@ -617,7 +617,7 @@ describe("Zotero.Sync.Runner", function () {
}); });
setResponse({ setResponse({
method: "GET", method: "GET",
url: "users/1/publications/fulltext", url: "users/1/publications/fulltext?format=versions",
status: 200, status: 200,
headers: { headers: {
"Last-Modified-Version": 10 "Last-Modified-Version": 10
@ -626,7 +626,7 @@ describe("Zotero.Sync.Runner", function () {
}); });
setResponse({ setResponse({
method: "GET", method: "GET",
url: "groups/1623562/fulltext", url: "groups/1623562/fulltext?format=versions",
status: 200, status: 200,
headers: { headers: {
"Last-Modified-Version": 15 "Last-Modified-Version": 15
@ -635,7 +635,7 @@ describe("Zotero.Sync.Runner", function () {
}); });
setResponse({ setResponse({
method: "GET", method: "GET",
url: "groups/2694172/fulltext", url: "groups/2694172/fulltext?format=versions",
status: 200, status: 200,
headers: { headers: {
"Last-Modified-Version": 20 "Last-Modified-Version": 20
@ -780,7 +780,7 @@ describe("Zotero.Sync.Runner", function () {
}); });
setResponse({ setResponse({
method: "GET", method: "GET",
url: "users/1/publications/fulltext", url: "users/1/publications/fulltext?format=versions",
status: 200, status: 200,
headers: { headers: {
"Last-Modified-Version": 5 "Last-Modified-Version": 5