Wiki source code of Porter
Last modified by Thomas Mortagne on 2018/02/05 18:03
Show last authors
author | version | line-number | content |
---|---|---|---|
1 | {{groovy}} | ||
2 | import com.xpn.xwiki.doc.XWikiAttachment; | ||
3 | import com.xpn.xwiki.doc.XWikiDocument; | ||
4 | import com.xpn.xwiki.doc.DeletedAttachment; | ||
5 | import org.xwiki.store.legacy.doc.internal.ListAttachmentArchive; | ||
6 | import com.xpn.xwiki.store.XWikiAttachmentStoreInterface; | ||
7 | import org.xwiki.store.legacy.store.internal.FilesystemAttachmentStore; | ||
8 | import com.xpn.xwiki.store.AttachmentVersioningStore; | ||
9 | import org.xwiki.store.legacy.store.internal.XWikiHibernateTransaction; | ||
10 | import com.xpn.xwiki.store.AttachmentRecycleBinStore; | ||
11 | import com.xpn.xwiki.web.Utils; | ||
12 | import com.xpn.xwiki.XWikiContext; | ||
13 | import com.xpn.xwiki.XWiki; | ||
14 | import com.xpn.xwiki.XWikiException; | ||
15 | import org.xwiki.store.filesystem.internal.FilesystemStoreTools; | ||
16 | import org.apache.commons.lang.exception.ExceptionUtils; | ||
17 | import org.hibernate.ObjectNotFoundException; | ||
18 | |||
19 | public void printIntro(final XWiki wiki) | ||
20 | { | ||
21 | println("= Filesystem attachment storage porting script ="); | ||
22 | |||
23 | println("== Step 1: Switch to Filesystem attachments. =="); | ||
24 | |||
25 | boolean ret = isFilesystemStore(wiki.getAttachmentStore()) \ | ||
26 | && isFilesystemStore(wiki.getAttachmentVersioningStore()) \ | ||
27 | && isFilesystemStore(wiki.getAttachmentRecycleBinStore()); | ||
28 | |||
29 | println(getStoreLine(isFilesystemStore(wiki.getAttachmentStore()), | ||
30 | wiki.getAttachmentStore())); | ||
31 | println(getStoreLine(isFilesystemStore(wiki.getAttachmentVersioningStore()), | ||
32 | wiki.getAttachmentVersioningStore())); | ||
33 | println(getStoreLine(isFilesystemStore(wiki.getAttachmentRecycleBinStore()), | ||
34 | wiki.getAttachmentRecycleBinStore())); | ||
35 | |||
36 | if (!ret) { | ||
37 | println("Please edit your xwiki.cfg file by modifying " | ||
38 | + "the attachment store lines to read as follows:"); | ||
39 | println("xwiki.store.attachment.hint = file"); | ||
40 | println("xwiki.store.attachment.versioning.hint = file"); | ||
41 | println("xwiki.store.attachment.recyclebin.hint = file"); | ||
42 | println("Also make sure they are not commented out."); | ||
43 | } | ||
44 | |||
45 | println("== Step 2: Add this directory to your backup routine. =="); | ||
46 | println("This is your storage directory, " | ||
47 | + "when filesystem attachments are enabled you have to back this up " | ||
48 | + "as part of your data backup routine.\n"); | ||
49 | println(Utils.getComponent(FilesystemStoreTools.class).storageDir.getAbsolutePath()); | ||
50 | |||
51 | println("== Step 3: Copy attachments from database to filesystem. =="); | ||
52 | println("Now you are ready to copy the data over from your database to the filesystem. " | ||
53 | + "It is prudent to leave the attachments in the database since this is still experimental " | ||
54 | + "and in most situations the attachment data is not bothersome just sitting in the " | ||
55 | + "database. As such, this script contains no facility to delete entries from the database."); | ||
56 | println("If anything goes wrong in this function, it will fail with an error message, " | ||
57 | + "you should get the stack trace (probably in the log file) and keep it to " | ||
58 | + "confuse and humiliate the developers with. No harm should be done since this only loads " | ||
59 | + "from the database and only saves to the filesystem."); | ||
60 | } | ||
61 | |||
62 | if (checkVersion()) { | ||
63 | mainEntryPoint(); | ||
64 | } | ||
65 | |||
66 | public boolean checkVersion() | ||
67 | { | ||
68 | String version = xwiki.version | ||
69 | String[] versionElements = version.split('[\\.-]') | ||
70 | |||
71 | int major = versionElements[0].toInteger() | ||
72 | if (major < 9) | ||
73 | return true | ||
74 | |||
75 | if (major == 9) { | ||
76 | int minor = versionElements[1].toInteger() | ||
77 | if (minor < 10) { | ||
78 | return true | ||
79 | } | ||
80 | } | ||
81 | |||
82 | println """ | ||
83 | {{error}} | ||
84 | This script work only for versions older than 9.10-rc-1 (current is $xwiki.version). Since 9.10-rc-1 XWiki support mixed storage for attachments so there is no need to moved them anymore. | ||
85 | {{/error}}""" | ||
86 | |||
87 | return false | ||
88 | } | ||
89 | |||
90 | public void mainEntryPoint() | ||
91 | { | ||
92 | final XWikiContext xc = xcontext.getContext(); | ||
93 | final XWiki wiki = xc.getWiki(); | ||
94 | |||
95 | int startAt = 0; | ||
96 | int seconds = 20; | ||
97 | boolean dryRun = true; | ||
98 | boolean verbose = false; | ||
99 | boolean go = request.getMethod().equals("POST") && request.getParameter("doIt") != null; | ||
100 | boolean ajax = "plain".equals(request.getParameter("xpage")); | ||
101 | int endedAt = 0; | ||
102 | |||
103 | if (!ajax) { | ||
104 | this.printIntro(wiki); | ||
105 | } | ||
106 | |||
107 | if (go) { | ||
108 | startAt = Integer.parseInt(request.getParameter("startAt")); | ||
109 | seconds = Integer.parseInt(request.getParameter("seconds")); | ||
110 | dryRun = "on".equals(request.getParameter("dryRun")); | ||
111 | verbose = "on".equals(request.getParameter("verbose")); | ||
112 | endedAt = this.doIt(startAt, seconds, dryRun, verbose, ajax); | ||
113 | if (ajax) { | ||
114 | if (endedAt == -1) { | ||
115 | this.printFooter(); | ||
116 | } else { | ||
117 | println("\n{{html clean=false}}<meta name='endedAt' content='" | ||
118 | + endedAt + "' />{{/html}}"); | ||
119 | } | ||
120 | return; | ||
121 | } | ||
122 | } | ||
123 | |||
124 | this.printJs(); | ||
125 | |||
126 | if (!go || endedAt > startAt) { | ||
127 | println("\n{{html clean=false}}"); | ||
128 | println("<div id='javascriptSpace'></div>"); | ||
129 | println("<form action='?time=" + System.nanoTime() | ||
130 | + "#doItForm' id='doItForm' method='POST'>"); | ||
131 | println("<input type='hidden' name='seconds' value='" + seconds + "' />"); | ||
132 | println("<input type='hidden' id='startAt' name='startAt' value='" + endedAt + "' />"); | ||
133 | println("<dl>"); | ||
134 | println("<dt><label for='dryRun'>Dry Run, don't save any files.</label></dt>"); | ||
135 | println("<dd><input type='checkbox' name='dryRun' checked='" | ||
136 | + ((dryRun) ? "checked" : "") + "' /></dd>"); | ||
137 | |||
138 | println("<dt><label for='verbose'>Verbose, show every attachment.</label></dt>"); | ||
139 | println("<dd><input type='checkbox' name='verbose' checked='" | ||
140 | + ((verbose) ? "checked" : "") + "' /></dd>"); | ||
141 | println("</dl>"); | ||
142 | println("<input type='hidden' name='doIt' value='yes' />") | ||
143 | println("<input type='submit' name='start' value='" | ||
144 | + ((endedAt > startAt) ? "Continue" : "Start") + "' />"); | ||
145 | println("</form>"); | ||
146 | println("<style>#xwikidata { display:none }</style>") | ||
147 | println("{{/html}}\n"); | ||
148 | return; | ||
149 | } | ||
150 | |||
151 | this.printFooter(); | ||
152 | } | ||
153 | |||
154 | public void printFooter() | ||
155 | { | ||
156 | println("== Step 4: make sure everything is working. =="); | ||
157 | println("Check to make sure your attachments are still there, if an attachment is broken, " | ||
158 | + "it will appear to be there but on opening it will tell you the attachment does not exist. " | ||
159 | + "If something goes terribly wrong with filesystem attachments, you may have your old " | ||
160 | + "attachment system back simply by changing the lines in the xwiki.cfg file and restarting " | ||
161 | + "the container. HOWEVER: This will not preserve attachments which were uploaded **after** " | ||
162 | + "switching to filesystem attachments so as users edit the system you will become " | ||
163 | + "locked in to filesystem attachments unless a script is written to do the " | ||
164 | + "inverse of this one."); | ||
165 | println("Despite being experimental, the filesystem attachment storage is quite stable and the " | ||
166 | + "risk of actually losing something so that it is unrecoverable is very remote."); | ||
167 | println("NOTE: This must be run seperately in each subwiki."); | ||
168 | println("\n\nEnjoy and give feedback."); | ||
169 | |||
170 | } | ||
171 | |||
172 | public static boolean isFilesystemStore(final Object store) | ||
173 | { | ||
174 | return store.getClass().getName().contains("Filesystem"); | ||
175 | } | ||
176 | |||
177 | public static String getStoreLine(final boolean isFilesystem, final Object store) | ||
178 | { | ||
179 | return "(%style='color:" + (isFilesystem ? "#080" : "#d00") + "'%)(((* " + store + ")))"; | ||
180 | } | ||
181 | |||
182 | public void printJs() | ||
183 | { | ||
184 | println("""\n{{html}}<script> | ||
185 | var isAtBottom = function() { | ||
186 | var vp = document.viewport; | ||
187 | return vp.getScrollOffsets().top + vp.getHeight() + 30 > window.body.getHeight(); | ||
188 | }; | ||
189 | |||
190 | Event.observe(document, "dom:loaded", function() { | ||
191 | var doItForm = document.getElementById("doItForm"); | ||
192 | var workspace = document.getElementById("javascriptSpace"); | ||
193 | var params = { | ||
194 | xpage: "plain", | ||
195 | seconds: doItForm.seconds.value, | ||
196 | startAt: doItForm.startAt.value, | ||
197 | doIt: "yes" | ||
198 | }; | ||
199 | var shouldContinue; | ||
200 | |||
201 | var doBlock = function(startAt) { | ||
202 | params.startAt = startAt; | ||
203 | new Ajax.Request(window.location.href, { | ||
204 | method: "POST", | ||
205 | parameters: params, | ||
206 | onSuccess: function(transport) { | ||
207 | var div = document.createElement("div"); | ||
208 | div.innerHTML = transport.responseText; | ||
209 | |||
210 | var wasAtBottom = isAtBottom(); | ||
211 | workspace.innerHTML += transport.responseText; | ||
212 | if (wasAtBottom || startAt == 0) { | ||
213 | doItForm.scrollTo(); | ||
214 | } | ||
215 | |||
216 | var metas = div.getElementsByTagName("meta"); | ||
217 | if (metas.length > 0 && shouldContinue) { | ||
218 | doBlock(metas[0].content); | ||
219 | return; | ||
220 | } else if (metas.length > 0) { | ||
221 | doItForm.startAt.value = metas[0].content; | ||
222 | doItForm.start.value = "Continue"; | ||
223 | } else { | ||
224 | doItForm.parentNode.removeChild(doItForm); | ||
225 | } | ||
226 | reachedEnd(); | ||
227 | } | ||
228 | }); | ||
229 | }; | ||
230 | |||
231 | var reachedEnd = function() { | ||
232 | Event.observe(doItForm, "submit", start); | ||
233 | Event.stopObserving(doItForm, "submit", stop); | ||
234 | }; | ||
235 | |||
236 | var stop = function(ev) { | ||
237 | ev.stop(); | ||
238 | shouldContinue = false; | ||
239 | reachedEnd(); | ||
240 | }; | ||
241 | |||
242 | var start = function(ev) { | ||
243 | ev.stop(); | ||
244 | params.dryRun = (doItForm.dryRun.checked) ? "on" : undefined; | ||
245 | params.verbose = (doItForm.verbose.checked) ? "on" : undefined; | ||
246 | workspace.innerHTML = ""; | ||
247 | shouldContinue = true; | ||
248 | doBlock(doItForm.startAt.value); | ||
249 | doItForm.start.value = "Abort"; | ||
250 | Event.stopObserving(doItForm, "submit", start); | ||
251 | Event.observe(doItForm, "submit", stop); | ||
252 | }; | ||
253 | |||
254 | Event.observe(doItForm, "submit", start); | ||
255 | }); | ||
256 | </script>{{/html}}"""); | ||
257 | } | ||
258 | |||
259 | /** | ||
260 | * Port all attachments from the original form to filesystem attachment store. | ||
261 | * Do one batch at a time so the user can see the progress and it won't time out. | ||
262 | * | ||
263 | * @param startAt the index of the first job to do, 0 to start at the beginning. | ||
264 | * @param seconds the number of seconds to run for before returning. | ||
265 | * @param dryRun don't save any files if this is true. | ||
266 | * @return the number of the first job which was not done. This can be startAt in the next call. | ||
267 | * -1 if the operation is complete. | ||
268 | */ | ||
269 | public int doIt(int startAt, int seconds, boolean dryRun, boolean verbose, boolean ajax) | ||
270 | { | ||
271 | final XWikiContext xc = this.xcontext.getContext(); | ||
272 | final XWiki wiki = xc.getWiki(); | ||
273 | final FilesystemAttachmentStore fileAttachStore = | ||
274 | Utils.getComponent(XWikiAttachmentStoreInterface.class, "file"); | ||
275 | final XWikiAttachmentStoreInterface defAttachStore = | ||
276 | Utils.getComponent(XWikiAttachmentStoreInterface.class, "hibernate"); | ||
277 | final AttachmentVersioningStore defAttachmentVersioningStore = | ||
278 | Utils.getComponent(AttachmentVersioningStore.class, "hibernate"); | ||
279 | |||
280 | int count = 0; | ||
281 | long stopTime = System.currentTimeMillis() + (1000 * seconds); | ||
282 | |||
283 | println("(%style='color:#00d'%)((("); | ||
284 | if (verbose && startAt == 0) { | ||
285 | println("\n== Attachments ==\n"); | ||
286 | } | ||
287 | for (String docName : xwiki.searchDocuments("where 1=1")) { | ||
288 | if (count < startAt) { | ||
289 | count++; | ||
290 | continue; | ||
291 | } else if (stopTime < System.currentTimeMillis()) { | ||
292 | println(")))"); | ||
293 | return count; | ||
294 | } else { | ||
295 | count++; | ||
296 | } | ||
297 | final XWikiDocument doc = xwiki.getDocument(docName).getDocument(); | ||
298 | try { | ||
299 | final XWikiHibernateTransaction transaction = new XWikiHibernateTransaction(xc); | ||
300 | |||
301 | if (verbose) { | ||
302 | println("* [[" + doc.getFullName() + "]]"); | ||
303 | } | ||
304 | // This is the equivilant of doc.getAttachmentList() but there is an issue which | ||
305 | // causes some attachments to be lost. | ||
306 | // http://jira.xwiki.org/browse/XWIKI-7936 | ||
307 | def hql = "select att from XWikiAttachment att WHERE att.docId=?"; | ||
308 | for (XWikiAttachment attach : xwiki.search(hql, [doc.getId()])) { | ||
309 | attach.setDoc(doc); | ||
310 | if (verbose) { | ||
311 | println("** [[attach:" + doc.getFullName() + "@" + attach.getFilename() + "]]"); | ||
312 | } | ||
313 | try { | ||
314 | fileAttachStore.loadAttachmentContent(attach, xc, false); | ||
315 | continue; | ||
316 | } catch (XWikiException noAttachmentInFilesystem) { | ||
317 | // no such attachment, let's save it. | ||
318 | } | ||
319 | try { | ||
320 | try { | ||
321 | defAttachStore.loadAttachmentContent(attach, xc, true); | ||
322 | } catch (XWikiException notInHibernate) { | ||
323 | final Throwable wrapped = notInHibernate.getException(); | ||
324 | if (wrapped && wrapped instanceof ObjectNotFoundException) { | ||
325 | println("(%style='color:#c80'%)((("); | ||
326 | println("Unable to find this attachment [[attach:" + attach.getFilename() | ||
327 | + "@" + doc.getFullName() + "]] in the database, " | ||
328 | + "it may be the result of database corruption from a failed " | ||
329 | + "upload prior to the fixing of " | ||
330 | + "[[http://jira.xwiki.org/browse/XWIKI-5055]]"); | ||
331 | println("xwikiattachment_content.XWA_ID = " + attach.getId()); | ||
332 | println(")))"); | ||
333 | } | ||
334 | } | ||
335 | defAttachmentVersioningStore.loadArchive(attach, xc, true); | ||
336 | // http://jira.xwiki.org/jira/browse/XWIKI-6199 | ||
337 | attach.getAttachment_archive().getVersions(); | ||
338 | // Setup the runnables to do the save but don't save just yet. | ||
339 | fileAttachStore.getAttachmentContentSaveRunnable(attach, false, xc) | ||
340 | .runIn(transaction); | ||
341 | } catch (Exception e) { | ||
342 | println("(%style='color:#d00'%)((("); | ||
343 | println("Error in [" + doc.getFullName() + ":" + attach.getFilename() + "]" | ||
344 | + e.getMessage()); | ||
345 | println(ExceptionUtils.getStackTrace(e)); | ||
346 | println(")))"); | ||
347 | } | ||
348 | } | ||
349 | if (!dryRun) { | ||
350 | transaction.start(); | ||
351 | } | ||
352 | } catch (Exception e) { | ||
353 | println("(%style='color:#d00'%)((("); | ||
354 | println("Failed to port attachments for document [" + doc.getFullName() + "]"); | ||
355 | println(e.getMessage()); | ||
356 | println(ExceptionUtils.getStackTrace(e)); | ||
357 | println(")))"); | ||
358 | } | ||
359 | } | ||
360 | println(")))"); | ||
361 | if (startAt < count) { | ||
362 | int attachCount = | ||
363 | xwiki.search("select count(at) from XWikiDocument as doc, " | ||
364 | + "XWikiAttachment as at where at.docId = doc.id").get(0).intValue(); | ||
365 | println("**[" + attachCount + "] attachments.**"); | ||
366 | } | ||
367 | |||
368 | |||
369 | // And now for the deleted attachments. | ||
370 | final AttachmentRecycleBinStore fileBin = | ||
371 | Utils.getComponent(AttachmentRecycleBinStore.class, "file"); | ||
372 | final AttachmentRecycleBinStore defaultBin = | ||
373 | Utils.getComponent(AttachmentRecycleBinStore.class, "hibernate"); | ||
374 | |||
375 | if (verbose && startAt < count) { | ||
376 | println("\n== Deleted Attachments ==\n"); | ||
377 | } | ||
378 | List<Object[]> entries = | ||
379 | xwiki.search("select da.id, da.docName, da.filename, da.date from DeletedAttachment as da"); | ||
380 | if (verbose && entries.size() == 0) { | ||
381 | println("\nnone."); | ||
382 | } | ||
383 | int delAttachCount = 0; | ||
384 | for (Object[] o : entries) { | ||
385 | long delAttachId = o[0]; | ||
386 | final String docName = o[1]; | ||
387 | final String fileName = o[2]; | ||
388 | final Date deleteDate = o[3]; | ||
389 | delAttachCount++; | ||
390 | if (count < startAt) { | ||
391 | if (verbose && !ajax) { | ||
392 | println("* [[" + docName + "]] - " + fileName); | ||
393 | } | ||
394 | count++; | ||
395 | continue; | ||
396 | } else if (stopTime < System.currentTimeMillis()) { | ||
397 | return count; | ||
398 | } | ||
399 | count++; | ||
400 | |||
401 | try { | ||
402 | final DeletedAttachment delAttach = | ||
403 | defaultBin.getDeletedAttachment(delAttachId, xc, true); | ||
404 | // Can't use the transaction runnable here because (non-filesystem) | ||
405 | // deleted attachments hold all content in memory so they have to be | ||
406 | // handled one at a time. | ||
407 | final XWikiAttachment attach = delAttach.restoreAttachment(null, xc); | ||
408 | try { | ||
409 | // http://jira.xwiki.org/jira/browse/XWIKI-6199 | ||
410 | attach.getAttachment_archive().getVersions(); | ||
411 | } catch (NullPointerException e) { | ||
412 | attach.setAttachment_archive(new ListAttachmentArchive([attach])); | ||
413 | } | ||
414 | if (!dryRun) { | ||
415 | fileBin.saveToRecycleBin(attach, | ||
416 | delAttach.getDeleter(), | ||
417 | delAttach.getDate(), | ||
418 | xc, | ||
419 | false); | ||
420 | } | ||
421 | |||
422 | if (verbose) { | ||
423 | println("* [[" + docName + "]]"); | ||
424 | println("** " + fileName + " - Deleted: " + deleteDate); | ||
425 | } | ||
426 | } catch (Exception e) { | ||
427 | println("(%style='color:#d00'%)((("); | ||
428 | println("* [[" + docName + "]] - " + fileName + " - Deleted: " + deleteDate); | ||
429 | println(e.getMessage()); | ||
430 | println(")))"); | ||
431 | println(ExceptionUtils.getStackTrace(e)); | ||
432 | } | ||
433 | } | ||
434 | println("\n**[" + delAttachCount + "] deleted attachments.**"); | ||
435 | return -1; | ||
436 | } | ||
437 | {{/groovy}} |