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