|
| 1 | + |
| 2 | +Components.utils.importGlobalProperties(['PathUtils', 'IOUtils']); |
| 3 | + |
| 4 | +Zotero.ZotFileRecovery = { |
| 5 | + id: null, |
| 6 | + version: null, |
| 7 | + rootURI: null, |
| 8 | + initialized: false, |
| 9 | + |
| 10 | + init({ id, version, rootURI }) { |
| 11 | + if(this.initialized) return; |
| 12 | + |
| 13 | + this.id = id; |
| 14 | + this.version = version; |
| 15 | + this.rootURI = rootURI; |
| 16 | + this.initialized = true; |
| 17 | + |
| 18 | + }, |
| 19 | + |
| 20 | + getTabletInfo (att, key) { |
| 21 | + try { |
| 22 | + var parser = new DOMParser(); |
| 23 | + var value, |
| 24 | + content = att.getNote(), |
| 25 | + doc = parser.parseFromString(content, 'text/html'), |
| 26 | + p = doc.querySelector('#zotfile-data'); |
| 27 | + if(p === null) p = doc.querySelector('[title*="lastmod"][title*="projectFolder"]'); |
| 28 | + if(p === null) { |
| 29 | + // support for old system |
| 30 | + var search = content.search(key); |
| 31 | + value = content.substring(search); |
| 32 | + value = value.substring(value.search('{') + 1, value.search('}')); |
| 33 | + } |
| 34 | + else { |
| 35 | + var data = JSON.parse(p.getAttribute('title').replace(/"/g, '"')); |
| 36 | + value = key in data ? data[key] : undefined; |
| 37 | + } |
| 38 | + // for location tag: replace [BaseFolder] with destination folder |
| 39 | + if(key == 'location') { |
| 40 | + let dest_dir = Zotero.Prefs.get('extensions.zotfile.tablet.dest_dir', true); |
| 41 | + dest_dir = dest_dir === undefined ? "" : dest_dir; |
| 42 | + value = value.replace('[BaseFolder]', dest_dir); |
| 43 | + } |
| 44 | + // for location and projectFolder tag: correct window/mac file system |
| 45 | + if(['location', 'projectFolder'].includes(key) && Zotero.isWin) value = value.replace(/\//g, '\\'); |
| 46 | + if(['location', 'projectFolder'].includes(key) && !Zotero.isWin) value = value.replace(/\\/g, '/'); |
| 47 | + // return |
| 48 | + return value; |
| 49 | + } |
| 50 | + catch (err) { |
| 51 | + return ''; |
| 52 | + } |
| 53 | + }, |
| 54 | + |
| 55 | + clearInfo (att) { |
| 56 | + try { |
| 57 | + var parser = new DOMParser(); |
| 58 | + var content = att.getNote().replace(/zotero:\/\//g, 'http://zotfile.com/'), |
| 59 | + doc = parser.parseFromString(content, 'text/html'), |
| 60 | + p = doc.querySelector('#zotfile-data'); |
| 61 | + if(p === null) p = doc.querySelector('[title*="lastmod"][title*="projectFolder"]'); |
| 62 | + if (p !== null) doc.removeChild(p); |
| 63 | + // save content back to note |
| 64 | + content = doc.documentElement.innerHTML |
| 65 | + // remove old zotfile data |
| 66 | + .replace(/(lastmod|mode|location|projectFolder)\{.*?\};?/g,'') |
| 67 | + // replace links with zotero links |
| 68 | + .replace(/http:\/\/zotfile.com\//g, 'zotero://'); |
| 69 | + att.setNote(content); |
| 70 | + } |
| 71 | + catch(e) { |
| 72 | + att.setNote(''); |
| 73 | + } |
| 74 | + }, |
| 75 | + |
| 76 | + _getSelectedAttachments() { |
| 77 | + let atts = Zotero.getActiveZoteroPane().getSelectedItems() |
| 78 | + .map(item => item.isRegularItem() ? item.getAttachments() : item) |
| 79 | + .reduce((a, b) => a.concat(b), []) |
| 80 | + .map(item => typeof item == 'number' ? Zotero.Items.get(item) : item) |
| 81 | + .filter(item => item.isAttachment()) |
| 82 | + .filter(item => this.getTabletInfo(item, 'mode') == 1); |
| 83 | + return atts; |
| 84 | + }, |
| 85 | + |
| 86 | + removeTabletTag(att, tag) { |
| 87 | + // remove from attachment |
| 88 | + att.removeTag(tag); |
| 89 | + // remove from parent item |
| 90 | + var item = Zotero.Items.get(att.parentItemID); |
| 91 | + if(item.hasTag(tag)) { |
| 92 | + var atts = Zotero.Items.get(item.getAttachments()); |
| 93 | + if(!atts.some(att => att.hasTag(tag))) |
| 94 | + item.removeTag(tag); |
| 95 | + } |
| 96 | + }, |
| 97 | + |
| 98 | + // async recoverTabletFile() { |
| 99 | + async recoverTabletFile() { |
| 100 | + Zotero.debug("ZotFileRecovery: Running 'recoverTabletFile'"); |
| 101 | + // get selected attachments |
| 102 | + let atts = this._getSelectedAttachments(); |
| 103 | + // filter by tablet tag |
| 104 | + let tablet_tag = Zotero.Prefs.get('extensions.zotfile.tablet.tag', true); |
| 105 | + tablet_tag = tablet_tag === undefined ? "_tablet" : tablet_tag; |
| 106 | + atts = atts.filter(att => att.hasTag(tablet_tag)); |
| 107 | + if(atts.length == 0) return; |
| 108 | + // get attachments from tablet |
| 109 | + atts = atts.map(att => this.getAttachmentFromTablet(att)); |
| 110 | + return; |
| 111 | + }, |
| 112 | + |
| 113 | + async getTabletFilePath(att) { |
| 114 | + Zotero.debug("ZotFileRecovery: Running 'getTabletFilePath'"); |
| 115 | + // foreground mode |
| 116 | + if(this.getTabletInfo(att, 'mode') == 2) |
| 117 | + return await att.getFilePathAsync(); |
| 118 | + // background mode |
| 119 | + var path = this.getTabletInfo(att, 'location'); |
| 120 | + return path; |
| 121 | + }, |
| 122 | + |
| 123 | + promptUser(message,but_0,but_1_cancel,but_2, title) { |
| 124 | + var title = typeof title !== 'ZotFile Dialog' ? title : true; |
| 125 | + var prompts = Components.classes['@mozilla.org/embedcomp/prompt-service;1'] |
| 126 | + .getService(Components.interfaces.nsIPromptService); |
| 127 | + |
| 128 | + var check = {value: false}; // default the checkbox to false |
| 129 | + |
| 130 | + var flags = prompts.BUTTON_POS_0 * prompts.BUTTON_TITLE_IS_STRING + |
| 131 | + prompts.BUTTON_POS_1 * prompts.BUTTON_TITLE_IS_STRING + |
| 132 | + prompts.BUTTON_POS_2 * prompts.BUTTON_TITLE_IS_STRING; |
| 133 | + |
| 134 | + var button = prompts.confirmEx(null, title, message, |
| 135 | + flags, but_0,but_1_cancel,but_2, null, check); |
| 136 | + |
| 137 | + return(button); |
| 138 | + |
| 139 | + }, |
| 140 | + |
| 141 | + getPref(pref, def) { |
| 142 | + let value = Zotero.Prefs.get('extensions.zotfile.' + pref, true); |
| 143 | + value = value === undefined ? def : value;; |
| 144 | + return value; |
| 145 | + }, |
| 146 | + |
| 147 | + // async getAttachmentFromTablet(att) { |
| 148 | + async getAttachmentFromTablet(att) { |
| 149 | + Zotero.debug("ZotFileRecovery: Running 'getAttachmentFromTablet'"); |
| 150 | + var item = Zotero.Items.get(att.parentItemID), |
| 151 | + tablet_tag = this.getPref('tablet.tag', '_tablet'), |
| 152 | + tablet_tagMod = this.getPref('tablet.tagModified', '_tablet_modified'), |
| 153 | + tag_parent = this.getPref('tablet.tagParentPush_tag', '_tablet_parent'); |
| 154 | + // Zotero and tablet file paths |
| 155 | + var path_zotero = await att.getFilePathAsync(), |
| 156 | + path_tablet = await this.getTabletFilePath(att, false); |
| 157 | + Zotero.debug("ZotFileRecovery 'path_tablet': " + path_tablet); |
| 158 | + Zotero.debug("ZotFileRecovery 'path_zotero':" + path_zotero); |
| 159 | + var path_tablet_exists = await IOUtils.exists(path_tablet); |
| 160 | + if (path_tablet == '' | !path_tablet_exists) { |
| 161 | + this.removeTabletTag(att, tablet_tag); |
| 162 | + this.clearInfo(att); |
| 163 | + await att.saveTx(); |
| 164 | + await item.saveTx(); |
| 165 | + this.infoWindow('ZotFile Warning', 'The tablet file "' + att.attachmentFilename + '" was manually moved and does not exist.'); |
| 166 | + return att; |
| 167 | + } |
| 168 | + // get modification times for files |
| 169 | + var time_tablet = 0, time_zotero = 0; |
| 170 | + if(await IOUtils.exists(path_tablet)) { |
| 171 | + let tablet_file_stat = await IOUtils.stat(path_tablet); |
| 172 | + time_tablet = tablet_file_stat.lastModified; |
| 173 | + } |
| 174 | + var time_saved = parseInt(this.getTabletInfo(att, 'lastmod'), 10); |
| 175 | + if(await IOUtils.exists(path_zotero)) { |
| 176 | + let zotero_file_stat = await IOUtils.stat(path_zotero); |
| 177 | + time_zotero = zotero_file_stat.lastModified; |
| 178 | + } |
| 179 | + Zotero.debug("ZotFileRecovery lastModified (tablet, saved, zotero): " + time_tablet + " " + time_saved + " " + time_zotero); |
| 180 | + // background mode |
| 181 | + if((time_tablet == 0 & time_zotero == 0)) { |
| 182 | + this.infoWindow('ZotFile Warning', 'Recovery of tablet file "' + att.attachmentFilename + '" failed. Please manually recover the file at "' + path_tablet + '"'); |
| 183 | + return att; |
| 184 | + } |
| 185 | + |
| 186 | + // Status of tablet file 'tablet_status' |
| 187 | + // 0 - Tablet file modified -> Replace zotero file |
| 188 | + // 1 - Both files modified -> Prompt user |
| 189 | + // 2 - Zotero file modified -> Delete tablet file |
| 190 | + var tablet_status = 1; |
| 191 | + if (time_tablet > time_saved && time_zotero <= time_saved) tablet_status = 0; |
| 192 | + if (time_tablet <= time_saved && time_zotero <= time_saved) tablet_status = 2; |
| 193 | + if (time_tablet <= time_saved && time_zotero > time_saved) tablet_status = 2; |
| 194 | + if (time_tablet > time_saved && time_zotero > time_saved) tablet_status = 1; |
| 195 | + Zotero.debug("ZotFileRecovery 'tablet_status': " + tablet_status); |
| 196 | + |
| 197 | + // prompt if both file have been modified |
| 198 | + if (tablet_status == 1) { |
| 199 | + let message = "Both copies of the attachment file '" + att.attachmentFilename + "' have been modified. What do you want to do?\n\nRemoving the tablet file discards all changes made to the file on the tablet."; |
| 200 | + tablet_status = this.promptUser(message, "Replace Zotero File", "Cancel", "Remove Tablet File"); |
| 201 | + if (tablet_status == 1) return; |
| 202 | + } |
| 203 | + |
| 204 | + // Replace zotero file |
| 205 | + if(tablet_status == 0) |
| 206 | + await IOUtils.move(path_tablet, path_zotero); |
| 207 | + // Remove tablet file |
| 208 | + if(tablet_status == 2) |
| 209 | + await Zotero.File.removeIfExists(path_tablet); |
| 210 | + |
| 211 | + // remove tag from attachment and parent item |
| 212 | + this.removeTabletTag(att, tablet_tag); |
| 213 | + if(item.hasTag(tag_parent)) item.removeTag(tag_parent); |
| 214 | + // clear attachment note |
| 215 | + this.Tablet.clearInfo(att); |
| 216 | + // remove modified tag from attachment |
| 217 | + this.removeTabletTag(att, this.Tablet.tagMod); |
| 218 | + await att.saveTx(); |
| 219 | + await item.saveTx(); |
| 220 | + // notification |
| 221 | + this.infoWindow('ZotFile Recovery', 'The tablet file "' + att.attachmentFilename + '" was removed from the tablet.'); |
| 222 | + // return... |
| 223 | + return att; |
| 224 | + }, |
| 225 | + |
| 226 | + infoWindow (headline, content) { |
| 227 | + // default arguments |
| 228 | + main = typeof main !== 'undefined' ? main : 'title'; |
| 229 | + message = typeof message !== 'undefined' ? message : 'message'; |
| 230 | + // show window |
| 231 | + var progressWin = new Zotero.ProgressWindow(); |
| 232 | + progressWin.changeHeadline(headline); |
| 233 | + progressWin.addLines(content); |
| 234 | + progressWin.show(); |
| 235 | + progressWin.startCloseTimer(); |
| 236 | + }, |
| 237 | + |
| 238 | + |
| 239 | +}; |
0 commit comments