syncFullTextEngine.js (6395B)
1 /* 2 ***** BEGIN LICENSE BLOCK ***** 3 4 Copyright © 2015 Center for History and New Media 5 George Mason University, Fairfax, Virginia, USA 6 http://zotero.org 7 8 This file is part of Zotero. 9 10 Zotero is free software: you can redistribute it and/or modify 11 it under the terms of the GNU Affero General Public License as published by 12 the Free Software Foundation, either version 3 of the License, or 13 (at your option) any later version. 14 15 Zotero is distributed in the hope that it will be useful, 16 but WITHOUT ANY WARRANTY; without even the implied warranty of 17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 GNU Affero General Public License for more details. 19 20 You should have received a copy of the GNU Affero General Public License 21 along with Zotero. If not, see <http://www.gnu.org/licenses/>. 22 23 ***** END LICENSE BLOCK ***** 24 */ 25 26 if (!Zotero.Sync.Data) { 27 Zotero.Sync.Data = {}; 28 } 29 30 Zotero.Sync.Data.FullTextEngine = function (options) { 31 if (options.apiClient == undefined) { 32 throw new Error("options.apiClient not set"); 33 } 34 if (options.libraryID == undefined) { 35 throw new Error("options.libraryID not set"); 36 } 37 38 this.MAX_BATCH_SIZE = 500000; 39 this.MAX_BATCH_ITEMS = 10; 40 41 this.apiClient = options.apiClient; 42 this.libraryID = options.libraryID; 43 this.library = Zotero.Libraries.get(options.libraryID); 44 this.setStatus = options.setStatus || function () {}; 45 this.onError = options.onError || function (e) {}; 46 this.stopOnError = options.stopOnError; 47 this._stopping = false; 48 this.failed = false; 49 } 50 51 Zotero.Sync.Data.FullTextEngine.prototype.start = Zotero.Promise.coroutine(function* () { 52 Zotero.debug("Starting full-text sync for " + this.library.name); 53 54 // Get last full-text version in settings 55 var libraryVersion = yield Zotero.FullText.getLibraryVersion(this.libraryID); 56 // If main library version has changed, check to see if there's no full-text content 57 if (this.library.libraryVersion > libraryVersion) { 58 yield this._download(libraryVersion); 59 } 60 else { 61 Zotero.debug("Library version hasn't changed -- skipping full-text download"); 62 } 63 64 this._stopCheck(); 65 66 yield this._upload(); 67 }) 68 69 70 Zotero.Sync.Data.FullTextEngine.prototype._download = Zotero.Promise.coroutine(function* (libraryVersion) { 71 Zotero.debug("Downloading full-text content for " + this.library.name); 72 73 // Get changed with ?since 74 var results = yield this.apiClient.getFullTextVersions( 75 this.library.libraryType, 76 this.library.libraryTypeID, 77 libraryVersion 78 ); 79 80 // Go through, checking local version against returned version 81 var keys = []; 82 for (let key in results.versions) { 83 let id = Zotero.Items.getIDFromLibraryAndKey(this.libraryID, key); 84 if (!id) { 85 Zotero.debug(`Skipping full-text for missing item ${this.library.name}`); 86 continue; 87 } 88 89 // Skip full text that's already up-to-date, which could happen due to a full sync or 90 // interrupted sync 91 let version = yield Zotero.Fulltext.getItemVersion(id); 92 if (version == results.versions[key]) { 93 Zotero.debug(`Skipping up-to-date full text for ${this.library.name}`); 94 continue; 95 } 96 keys.push(key); 97 } 98 99 this.requestPromises = []; 100 101 yield Zotero.Promise.map( 102 keys, 103 (key) => { 104 this._stopCheck(); 105 return this.apiClient.getFullTextForItem( 106 this.library.libraryType, this.library.libraryTypeID, key 107 ) 108 .then((results) => { 109 this._stopCheck(); 110 if (!results) return; 111 return Zotero.Fulltext.setItemContent( 112 this.libraryID, key, results.data, results.version 113 ) 114 }) 115 }, 116 // Prepare twice the number of concurrent requests 117 { concurrency: 8 } 118 ); 119 120 yield Zotero.FullText.setLibraryVersion(this.libraryID, results.libraryVersion); 121 }); 122 123 124 Zotero.Sync.Data.FullTextEngine.prototype._upload = Zotero.Promise.coroutine(function* () { 125 if (!this.library.editable) return; 126 127 Zotero.debug("Uploading full-text content for " + this.library.name); 128 129 var libraryVersion = this.library.libraryVersion; 130 var props = ['key', 'content', 'indexedChars', 'totalChars', 'indexedPages', 'totalPages']; 131 132 let lastItemID = 0; 133 while (true) { 134 this._stopCheck(); 135 136 let objs = yield Zotero.FullText.getUnsyncedContent(this.libraryID, { 137 maxSize: this.MAX_BATCH_SIZE, 138 maxItems: this.MAX_BATCH_ITEMS, 139 lastItemID 140 }); 141 if (!objs.length) { 142 break; 143 } 144 145 let jsonArray = []; 146 let results; 147 148 for (let obj of objs) { 149 let json = {}; 150 for (let prop of props) { 151 json[prop] = obj[prop]; 152 } 153 jsonArray.push(json); 154 lastItemID = obj.itemID; 155 } 156 ({ libraryVersion, results } = yield this.apiClient.setFullTextForItems( 157 this.library.libraryType, 158 this.library.libraryTypeID, 159 libraryVersion, 160 jsonArray 161 )); 162 yield Zotero.DB.executeTransaction(function* () { 163 for (let state of ['successful', 'unchanged']) { 164 for (let index in results[state]) { 165 let key = results[state][index].key; 166 let itemID = Zotero.Items.getIDFromLibraryAndKey(this.libraryID, key); 167 yield Zotero.FullText.setItemSynced(itemID, libraryVersion); 168 } 169 } 170 // Set both the library version and the full-text library version. The latter is necessary 171 // because full-text sync can be turned off at any time, so we have to keep track of the 172 // last version we've seen for full-text in case the main library version has advanced since. 173 yield Zotero.FullText.setLibraryVersion(this.libraryID, libraryVersion); 174 this.library.libraryVersion = libraryVersion; 175 yield this.library.save(); 176 }.bind(this)); 177 178 for (let index in results.failed) { 179 let { code, message, data } = results.failed[index]; 180 let e = new Error(message); 181 e.name = "ZoteroObjectUploadError"; 182 e.code = code; 183 if (data) { 184 e.data = data; 185 } 186 Zotero.logError("Error uploading full text for item " + jsonArray[index].key + " in " 187 + this.library.name + ":\n\n" + e); 188 189 if (this.onError) { 190 this.onError(e); 191 } 192 if (this.stopOnError) { 193 throw new Error(e); 194 } 195 } 196 } 197 }); 198 199 200 Zotero.Sync.Data.FullTextEngine.prototype.stop = Zotero.Promise.coroutine(function* () { 201 // TODO: Cancel requests? 202 this._stopping = true; 203 }) 204 205 206 Zotero.Sync.Data.FullTextEngine.prototype._stopCheck = function () { 207 if (!this._stopping) return; 208 Zotero.debug("Full-text sync stopped for " + this.library.name); 209 throw new Zotero.Sync.UserCancelledException; 210 }