· 8 years ago · Jul 02, 2018, 01:10 PM
1Index: includes/AutoLoader.php
2===================================================================
3--- includes/AutoLoader.php (revision 113170)
4+++ includes/AutoLoader.php (working copy)
5@@ -507,6 +507,9 @@
6 'FSFileBackendFileList' => 'includes/filerepo/backend/FSFileBackend.php',
7 'SwiftFileBackend' => 'includes/filerepo/backend/SwiftFileBackend.php',
8 'SwiftFileBackendFileList' => 'includes/filerepo/backend/SwiftFileBackend.php',
9+ 'FileJournal' => 'includes/filerepo/backend/filejournal/FileJournal.php',
10+ 'DBFileJournal' => 'includes/filerepo/backend/filejournal/DBFileJournal.php',
11+ 'NullFileJournal' => 'includes/filerepo/backend/filejournal/FileJournal.php',
12 'LockManagerGroup' => 'includes/filerepo/backend/lockmanager/LockManagerGroup.php',
13 'LockManager' => 'includes/filerepo/backend/lockmanager/LockManager.php',
14 'ScopedLock' => 'includes/filerepo/backend/lockmanager/LockManager.php',
15Index: includes/filerepo/backend/FileBackend.php
16===================================================================
17--- includes/filerepo/backend/FileBackend.php (revision 113170)
18+++ includes/filerepo/backend/FileBackend.php (working copy)
19@@ -45,6 +45,8 @@
20 protected $readOnly; // string; read-only explanation message
21 /** @var LockManager */
22 protected $lockManager;
23+ /** @var FileJournal */
24+ protected $fileJournal;
25
26 /**
27 * Create a new backend instance from configuration.
28@@ -73,6 +75,9 @@
29 $this->lockManager = ( $config['lockManager'] instanceof LockManager )
30 ? $config['lockManager']
31 : LockManagerGroup::singleton()->get( $config['lockManager'] );
32+ $this->fileJournal = isset( $config['fileJournal'] )
33+ ? FileJournal::factory( $config['fileJournal'], $this->name )
34+ : FileJournal::factory( array( 'class' => 'NullFileJournal' ), $this->name );
35 $this->readOnly = isset( $config['readOnly'] )
36 ? (string)$config['readOnly']
37 : '';
38@@ -177,6 +182,8 @@
39 * 'allowStale' : Don't require the latest available data.
40 * This can increase performance for non-critical writes.
41 * This has no effect unless the 'force' flag is set.
42+ * 'nonJournaled' : Don't log this operation batch in the file journal.
43+ * This limits the ability of recovery scripts.
44 *
45 * Remarks on locking:
46 * File system paths given to operations should refer to files that are
47Index: includes/filerepo/backend/FileBackendMultiWrite.php
48===================================================================
49--- includes/filerepo/backend/FileBackendMultiWrite.php (revision 113170)
50+++ includes/filerepo/backend/FileBackendMultiWrite.php (working copy)
51@@ -133,7 +133,7 @@
52 }
53
54 // Actually attempt the operation batch...
55- $subStatus = FileOp::attemptBatch( $performOps, $opts );
56+ $subStatus = FileOp::attemptBatch( $performOps, $opts, $this->fileJournal );
57
58 $success = array();
59 $failCount = 0;
60Index: includes/filerepo/backend/FileBackendStore.php
61===================================================================
62--- includes/filerepo/backend/FileBackendStore.php (revision 113170)
63+++ includes/filerepo/backend/FileBackendStore.php (working copy)
64@@ -707,7 +707,7 @@
65 $this->clearCache();
66
67 // Actually attempt the operation batch...
68- $subStatus = FileOp::attemptBatch( $performOps, $opts );
69+ $subStatus = FileOp::attemptBatch( $performOps, $opts, $this->fileJournal );
70
71 // Merge errors into status fields
72 $status->merge( $subStatus );
73Index: includes/filerepo/backend/filejournal/DBFileJournal.php
74===================================================================
75--- includes/filerepo/backend/filejournal/DBFileJournal.php (revision 0)
76+++ includes/filerepo/backend/filejournal/DBFileJournal.php (working copy)
77@@ -0,0 +1,113 @@
78+<?php
79+/**
80+ * @file
81+ * @ingroup FileJournal
82+ * @author Aaron Schulz
83+ */
84+
85+/**
86+ * Version of FileJournal that logs to a DB table
87+ * @since 1.20
88+ */
89+class DBFileJournal extends FileJournal {
90+ protected $wiki = false; // string; wiki DB name
91+
92+ /**
93+ * Construct a new instance from configuration.
94+ * $config includes:
95+ * 'wiki' : wiki name to use for LoadBalancer
96+ *
97+ * @param $config Array
98+ */
99+ protected function __construct( array $config ) {
100+ parent::__construct( $config );
101+
102+ $this->wiki = $config['wiki'];
103+ }
104+
105+ /**
106+ * @see FileJournal::logChangeBatch()
107+ * @return Status
108+ */
109+ protected function doLogChangeBatch( array $entries, $batchId ) {
110+ $status = Status::newGood();
111+
112+ $dbw = $this->getMasterDB();
113+ if ( !$dbw ) {
114+ $status->fatal( 'filejournal-fail-dbconnect', $this->backend );
115+ return $status;
116+ }
117+ $now = wfTimestamp( TS_UNIX );
118+
119+ $data = array();
120+ foreach ( $entries as $entry ) {
121+ $data[] = array(
122+ 'fj_batch_uuid' => $batchId,
123+ 'fj_backend' => $this->backend,
124+ 'fj_op' => $entry['op'],
125+ 'fj_path' => $entry['path'],
126+ 'fj_path_sha1' => wfBaseConvert( sha1( $entry['path'] ), 16, 36, 31 ),
127+ 'fj_old_sha1' => $entry['oldSha1'],
128+ 'fj_new_sha1' => $entry['newSha1'],
129+ 'fj_timestamp' => $dbw->timestamp( $now )
130+ );
131+ }
132+
133+ try {
134+ $dbw->begin();
135+ $dbw->insert( 'filejournal', $data, __METHOD__ );
136+ $dbw->commit();
137+ } catch ( DBError $e ) {
138+ $status->fatal( 'filejournal-fail-dbquery', $this->backend );
139+ return $status;
140+ }
141+
142+ return $status;
143+ }
144+
145+ /**
146+ * @see FileJournal::purgeOldLogs()
147+ * @return Status
148+ */
149+ protected function doPurgeOldLogs() {
150+ $status = Status::newGood();
151+ if ( $this->ttlDays <= 0 ) {
152+ return $status; // nothing to do
153+ }
154+
155+ $dbw = $this->getMasterDB();
156+ if ( !$dbw ) {
157+ $status->fatal( 'filejournal-fail-dbconnect', $this->backend );
158+ return $status;
159+ }
160+ $dbCutoff = $dbw->timestamp( time() - 86400 * $this->ttlDays );
161+
162+ try {
163+ $dbw->begin();
164+ $dbw->delete( 'filejournal',
165+ array( 'fj_timestamp < ' . $dbw->addQuotes( $dbCutoff ) ),
166+ __METHOD__
167+ );
168+ $dbw->commit();
169+ } catch ( DBError $e ) {
170+ $status->fatal( 'filejournal-fail-dbquery', $this->backend );
171+ return $status;
172+ }
173+
174+ return $status;
175+ }
176+
177+ /**
178+ * Get a master connection to the logging DB
179+ *
180+ * @return DatabaseBase|null
181+ */
182+ protected function getMasterDB() {
183+ try {
184+ $lb = wfGetLBFactory()->newMainLB();
185+ return $lb->getConnection( DB_MASTER, array(), $this->wiki );
186+ } catch ( DBConnectionError $e ) {
187+ return null;
188+ }
189+ }
190+}
191Index: includes/filerepo/backend/filejournal/DBFileJournal.php
192===================================================================
193--- includes/filerepo/backend/filejournal/DBFileJournal.php (revision 0)
194+++ includes/filerepo/backend/filejournal/DBFileJournal.php (working copy)
195
196Property changes on: includes/filerepo/backend/filejournal/DBFileJournal.php
197___________________________________________________________________
198Added: svn:eol-style
199## -0,0 +1 ##
200+native
201Index: includes/filerepo/backend/filejournal/FileJournal.php
202===================================================================
203--- includes/filerepo/backend/filejournal/FileJournal.php (revision 0)
204+++ includes/filerepo/backend/filejournal/FileJournal.php (working copy)
205@@ -0,0 +1,129 @@
206+<?php
207+/**
208+ * @defgroup FileJournal File journal
209+ * @ingroup FileBackend
210+ */
211+
212+/**
213+ * @file
214+ * @ingroup FileJournal
215+ * @author Aaron Schulz
216+ */
217+
218+/**
219+ * @brief Class for handling file operation journaling.
220+ *
221+ * Subclasses should avoid throwing exceptions at all costs.
222+ *
223+ * @ingroup FileJournal
224+ * @since 1.20
225+ */
226+abstract class FileJournal {
227+ protected $backend; // string
228+ protected $ttlDays; // integer
229+
230+ /**
231+ * Construct a new instance from configuration.
232+ * $config includes:
233+ * 'ttlDays' : days to keep log entries around (false means "forever")
234+ *
235+ * @param $config Array
236+ */
237+ protected function __construct( array $config ) {
238+ $this->ttlDays = isset( $config['ttlDays'] ) ? $config['ttlDays'] : false;
239+ }
240+
241+ /**
242+ * Create an appropriate FileJournal object from config
243+ *
244+ * @param $config Array
245+ * @param $backend string A registered file backend name
246+ * @return FileJournal
247+ */
248+ final public static function factory( array $config, $backend ) {
249+ $class = $config['class'];
250+ $jrn = new $class( $config );
251+ if ( !$jrn instanceof self ) {
252+ throw new MWException( "Class given is not an instance of FileJournal." );
253+ }
254+ $jrn->backend = $backend;
255+ return $jrn;
256+ }
257+
258+ /**
259+ * Get a statistically unique ID string
260+ *
261+ * @return string <9 char TS_MW timestamp in base 36><22 random base 36 chars>
262+ */
263+ final public function getTimestampedUUID() {
264+ $s = '';
265+ for ( $i = 0; $i < 5; $i++ ) {
266+ $s .= mt_rand( 0, 2147483647 );
267+ }
268+ $s = wfBaseConvert( sha1( $s ), 16, 36, 31 );
269+ return substr( wfBaseConvert( wfTimestamp( TS_MW ), 10, 36, 9 ) . $s, 0, 31 );
270+ }
271+
272+ /**
273+ * Log changes made by a batch file operation.
274+ * $entries is an array of log entries, each of which contains:
275+ * op : Basic operation name (create, store, copy, delete)
276+ * path : The storage path of the file
277+ * oldSha1 : The initial base 36 SHA-1 of the file
278+ * newSha1 : The final base 36 SHA-1 of the file
279+ * Note that 'false' should be used as the SHA-1 for non-existing files.
280+ *
281+ * @param $entries Array List of file operations (each an array of parameters)
282+ * @param $batchId string UUID string that identifies the operation batch
283+ * @return Status
284+ */
285+ final public function logChangeBatch( array $entries, $batchId ) {
286+ return $this->doLogChangeBatch( $entries, $batchId );
287+ }
288+
289+ /**
290+ * @see FileJournal::logChangeBatch()
291+ *
292+ * @param $entries Array List of file operations (each an array of parameters)
293+ * @param $batchId string UUID string that identifies the operation batch
294+ * @return Status
295+ */
296+ abstract protected function doLogChangeBatch( array $entries, $batchId );
297+
298+ /**
299+ * Purge any old log entries
300+ *
301+ * @return Status
302+ */
303+ final public function purgeOldLogs() {
304+ return $this->doPurgeOldLogs();
305+ }
306+
307+ /**
308+ * @see FileJournal::purgeOldLogs()
309+ * @return Status
310+ */
311+ abstract protected function doPurgeOldLogs();
312+}
313+
314+/**
315+ * Simple version of FileJournal that does nothing
316+ * @since 1.20
317+ */
318+class NullFileJournal extends FileJournal {
319+ /**
320+ * @see FileJournal::logChangeBatch()
321+ * @return Status
322+ */
323+ protected function doLogChangeBatch( array $entries, $batchId ) {
324+ return Status::newGood();
325+ }
326+
327+ /**
328+ * @see FileJournal::purgeOldLogs()
329+ * @return Status
330+ */
331+ protected function doPurgeOldLogs() {
332+ return Status::newGood();
333+ }
334+}
335Index: includes/filerepo/backend/filejournal/FileJournal.php
336===================================================================
337--- includes/filerepo/backend/filejournal/FileJournal.php (revision 0)
338+++ includes/filerepo/backend/filejournal/FileJournal.php (working copy)
339
340Property changes on: includes/filerepo/backend/filejournal/FileJournal.php
341___________________________________________________________________
342Added: svn:eol-style
343## -0,0 +1 ##
344+native
345Index: includes/filerepo/backend/FileOp.php
346===================================================================
347--- includes/filerepo/backend/FileOp.php (revision 113170)
348+++ includes/filerepo/backend/FileOp.php (working copy)
349@@ -20,10 +20,13 @@
350 protected $params = array();
351 /** @var FileBackendStore */
352 protected $backend;
353+ /** @var Array */
354+ protected $journalEntries = array();
355
356 protected $state = self::STATE_NEW; // integer
357 protected $failed = false; // boolean
358 protected $useLatest = true; // boolean
359+ protected $batchId; // string
360
361 protected $sourceSha1; // string
362 protected $destSameAsSource; // boolean
363@@ -63,6 +66,16 @@
364 }
365
366 /**
367+ * Set the batch UUID this operation belongs to
368+ *
369+ * @param $batchId string
370+ * @return void
371+ */
372+ final protected function setBatchId( $batchId ) {
373+ $this->batchId = $batchId;
374+ }
375+
376+ /**
377 * Allow stale data for file reads and existence checks
378 *
379 * @return void
380@@ -72,35 +85,42 @@
381 }
382
383 /**
384- * Attempt a series of file operations.
385+ * Attempt to perform a series of file operations.
386 * Callers are responsible for handling file locking.
387 *
388 * $opts is an array of options, including:
389- * 'force' : Errors that would normally cause a rollback do not.
390- * The remaining operations are still attempted if any fail.
391- * 'allowStale' : Don't require the latest available data.
392- * This can increase performance for non-critical writes.
393- * This has no effect unless the 'force' flag is set.
394+ * 'force' : Errors that would normally cause a rollback do not.
395+ * The remaining operations are still attempted if any fail.
396+ * 'allowStale' : Don't require the latest available data.
397+ * This can increase performance for non-critical writes.
398+ * This has no effect unless the 'force' flag is set.
399+ * 'nonJournaled' : Don't log this operation batch in the file journal.
400 *
401 * @param $performOps Array List of FileOp operations
402 * @param $opts Array Batch operation options
403+ * @param $journal FileJournal Journal to log operations to
404 * @return Status
405 */
406- final public static function attemptBatch( array $performOps, array $opts ) {
407+ final public static function attemptBatch(
408+ array $performOps, array $opts, FileJournal $journal
409+ ) {
410 $status = Status::newGood();
411
412- $allowStale = !empty( $opts['allowStale'] );
413- $ignoreErrors = !empty( $opts['force'] );
414-
415 $n = count( $performOps );
416 if ( $n > self::MAX_BATCH_SIZE ) {
417 $status->fatal( 'backend-fail-batchsize', $n, self::MAX_BATCH_SIZE );
418 return $status;
419 }
420
421+ $batchId = $journal->getTimestampedUUID();
422+ $allowStale = !empty( $opts['allowStale'] );
423+ $ignoreErrors = !empty( $opts['force'] );
424+
425+ $entries = array(); // file journal entries
426 $predicates = FileOp::newPredicates(); // account for previous op in prechecks
427 // Do pre-checks for each operation; abort on failure...
428 foreach ( $performOps as $index => $fileOp ) {
429+ $fileOp->setBatchId( $batchId );
430 if ( $allowStale ) {
431 $fileOp->allowStaleReads(); // allow potentially stale reads
432 }
433@@ -112,9 +132,19 @@
434 if ( !$ignoreErrors ) {
435 return $status; // abort
436 }
437+ } else { // nothing to log if failed
438+ $entries = array_merge( $entries, $fileOp->getJournalEntries() );
439 }
440 }
441
442+ // Log the operations in file journal...
443+ if ( empty( $opts['nonJournaled'] ) ) {
444+ $subStatus = $journal->logChangeBatch( $entries, $batchId );
445+ if ( !$subStatus->isOK() ) {
446+ return $subStatus; // abort
447+ }
448+ }
449+
450 // Restart PHP's execution timer and set the timeout to safe amount.
451 // This handles cases where the operations take a long time or where we are
452 // already running low on time left. The old timeout is restored afterwards.
453@@ -134,13 +164,12 @@
454 } else {
455 $status->success[$index] = false;
456 ++$status->failCount;
457- if ( !$ignoreErrors ) {
458- // Log remaining ops as failed for recovery...
459- for ( $i = ($index + 1); $i < count( $performOps ); $i++ ) {
460- $performOps[$i]->logFailure( 'attempt_aborted' );
461- }
462- return $status; // bail out
463+ // We can't continue (even with $ignoreErrors) as $predicates is wrong.
464+ // Log the remaining ops as failed for recovery...
465+ for ( $i = ($index + 1); $i < count( $performOps ); $i++ ) {
466+ $performOps[$i]->logFailure( 'attempt_aborted' );
467 }
468+ return $status; // bail out
469 }
470 }
471
472@@ -336,6 +365,18 @@
473 }
474
475 /**
476+ * Return an array to use as file journal entries for this operation
477+ *
478+ * @return Array
479+ */
480+ final protected function getJournalEntries() {
481+ if ( $this->state == self::STATE_NEW || $this->failed ) {
482+ throw new MWException( __METHOD__ . " called, but not after successful precheck()." );
483+ }
484+ return $this->journalEntries;
485+ }
486+
487+ /**
488 * Log a file operation failure and preserve any temp files
489 *
490 * @param $action string
491@@ -345,8 +386,8 @@
492 $params = $this->params;
493 $params['failedAction'] = $action;
494 try {
495- wfDebugLog( 'FileOperation',
496- get_class( $this ) . ' failed: ' . FormatJson::encode( $params ) );
497+ wfDebugLog( 'FileOperation', get_class( $this ) .
498+ " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) );
499 } catch ( Exception $e ) {
500 // bad config? debug log error?
501 }
502@@ -441,10 +482,20 @@
503 // Check if destination file exists
504 $status->merge( $this->precheckDestExistence( $predicates ) );
505 if ( $status->isOK() ) {
506+ // Set the file journal entries for this change
507+ $this->journalEntries = array(
508+ array(
509+ 'path' => $this->params['dst'],
510+ 'oldSha1' => $this->fileSha1( $this->params['dst'], $predicates ),
511+ 'newSha1' => $this->sourceSha1,
512+ 'op' => 'store'
513+ )
514+ );
515 // Update file existence predicates
516 $predicates['exists'][$this->params['dst']] = true;
517 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
518 }
519+
520 return $status; // safe to call attempt()
521 }
522
523@@ -499,6 +550,15 @@
524 // Check if destination file exists
525 $status->merge( $this->precheckDestExistence( $predicates ) );
526 if ( $status->isOK() ) {
527+ // Set the file journal entries for this change
528+ $this->journalEntries = array(
529+ array(
530+ 'path' => $this->params['dst'],
531+ 'oldSha1' => $this->fileSha1( $this->params['dst'], $predicates ),
532+ 'newSha1' => $this->sourceSha1,
533+ 'op' => 'create'
534+ )
535+ );
536 // Update file existence predicates
537 $predicates['exists'][$this->params['dst']] = true;
538 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
539@@ -551,6 +611,21 @@
540 // Check if destination file exists
541 $status->merge( $this->precheckDestExistence( $predicates ) );
542 if ( $status->isOK() ) {
543+ // Set the file journal entries for this change
544+ $this->journalEntries = array(
545+ array( // assertion for recovery
546+ 'path' => $this->params['src'],
547+ 'oldSha1' => $this->sourceSha1,
548+ 'newSha1' => $this->sourceSha1,
549+ 'op' => 'null'
550+ ),
551+ array(
552+ 'path' => $this->params['dst'],
553+ 'oldSha1' => $this->fileSha1( $this->params['dst'], $predicates ),
554+ 'newSha1' => $this->sourceSha1,
555+ 'op' => 'copy'
556+ )
557+ );
558 // Update file existence predicates
559 $predicates['exists'][$this->params['dst']] = true;
560 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
561@@ -606,6 +681,27 @@
562 // Check if destination file exists
563 $status->merge( $this->precheckDestExistence( $predicates ) );
564 if ( $status->isOK() ) {
565+ // Set the file journal entries for this change
566+ $this->journalEntries = array(
567+ array( // assertion for recovery
568+ 'path' => $this->params['src'],
569+ 'oldSha1' => $this->sourceSha1,
570+ 'newSha1' => $this->sourceSha1,
571+ 'op' => 'null'
572+ ),
573+ array( // first copied
574+ 'path' => $this->params['dst'],
575+ 'oldSha1' => $this->fileSha1( $this->params['dst'], $predicates ),
576+ 'newSha1' => $this->sourceSha1,
577+ 'op' => 'copy'
578+ ),
579+ array( // then deleted
580+ 'path' => $this->params['src'],
581+ 'oldSha1' => $this->sourceSha1,
582+ 'newSha1' => false,
583+ 'op' => 'delete'
584+ )
585+ );
586 // Update file existence predicates
587 $predicates['exists'][$this->params['src']] = false;
588 $predicates['sha1'][$this->params['src']] = false;
589@@ -663,6 +759,15 @@
590 }
591 $this->needsDelete = false;
592 }
593+ // Set the file journal entries for this change
594+ $this->journalEntries = array(
595+ array(
596+ 'path' => $this->params['src'],
597+ 'oldSha1' => $this->fileSha1( $this->params['src'], $predicates ),
598+ 'newSha1' => false,
599+ 'op' => 'delete'
600+ )
601+ );
602 // Update file existence predicates
603 $predicates['exists'][$this->params['src']] = false;
604 $predicates['sha1'][$this->params['src']] = false;
605Index: includes/installer/MysqlUpdater.php
606===================================================================
607--- includes/installer/MysqlUpdater.php (revision 113170)
608+++ includes/installer/MysqlUpdater.php (working copy)
609@@ -196,6 +196,7 @@
610 // 1.20
611 array( 'addTable', 'config', 'patch-config.sql' ),
612 array( 'addIndex', 'revision', 'page_user_timestamp', 'patch-revision-user-page-index.sql' ),
613+ array( 'addTable', 'filejournal', 'patch-filejournal.sql' ),
614 );
615 }
616
617Index: includes/installer/SqliteUpdater.php
618===================================================================
619--- includes/installer/SqliteUpdater.php (revision 113170)
620+++ includes/installer/SqliteUpdater.php (working copy)
621@@ -75,6 +75,7 @@
622 // 1.20
623 array( 'addTable', 'config', 'patch-config.sql' ),
624 array( 'addIndex', 'revision', 'page_user_timestamp', 'patch-revision-user-page-index.sql' ),
625+ array( 'addTable', 'filejournal', 'patch-filejournal.sql' ),
626 );
627 }
628
629Index: languages/messages/MessagesEn.php
630===================================================================
631--- languages/messages/MessagesEn.php (revision 113170)
632+++ languages/messages/MessagesEn.php (working copy)
633@@ -2270,6 +2270,10 @@
634 'backend-fail-contenttype' => 'Could not determine the content type of the file to store at "$1".',
635 'backend-fail-batchsize' => 'Storage backend given a batch of $1 file {{PLURAL:$1|operation|operations}}; the limit is $2 {{PLURAL:$2|operation|operations}}.',
636
637+# File journal
638+'filejournal-fail-dbconnect' => 'Could not connect to the journal database for storage backend "$1".',
639+'filejournal-fail-dbquery' => 'Could not update the journal database for storage backend "$1".',
640+
641 # Lock manager
642 'lockmanager-notlocked' => 'Could not unlock "$1"; it is not locked.',
643 'lockmanager-fail-closelock' => 'Could not close lock file for "$1".',
644Index: maintenance/archives/patch-filejournal.sql
645===================================================================
646--- maintenance/archives/patch-filejournal.sql (revision 0)
647+++ maintenance/archives/patch-filejournal.sql (working copy)
648@@ -0,0 +1,25 @@
649+-- File backend operation journal
650+CREATE TABLE /*_*/filejournal (
651+ -- Unique ID for each file operation
652+ fj_id bigint unsigned NOT NULL PRIMARY KEY auto_increment,
653+ -- UUID of the batch this operation belongs to
654+ fj_batch_uuid varbinary(32) NOT NULL,
655+ -- The registered file backend name
656+ fj_backend varchar(255) NOT NULL,
657+ -- The storage path that was affected (may be internal paths)
658+ fj_path blob NOT NULL,
659+ -- SHA-1 file path hash in base-36
660+ fj_path_sha1 varbinary(32) NOT NULL default '',
661+ -- Primitive operation description
662+ fj_op varchar(16) NOT NULL default '',
663+ -- SHA-1 file content hash in base-36 (old and new may be equal)
664+ fj_old_sha1 varbinary(32) NOT NULL default '',
665+ fj_new_sha1 varbinary(32) NOT NULL default '',
666+ -- Timestamp of the batch operation
667+ fj_timestamp varbinary(14) NOT NULL default ''
668+);
669+
670+CREATE INDEX /*i*/fj_batch_id ON /*_*/filejournal (fj_batch_uuid,fj_id);
671+CREATE INDEX /*i*/fj_path_id ON /*_*/filejournal (fj_path_sha1,fj_id);
672+CREATE INDEX /*i*/fj_new_sha1 ON /*_*/filejournal (fj_new_sha1,fj_id);
673+CREATE INDEX /*i*/fj_timestamp ON /*_*/filejournal (fj_timestamp);
674Index: maintenance/archives/patch-filejournal.sql
675===================================================================
676--- maintenance/archives/patch-filejournal.sql (revision 0)
677+++ maintenance/archives/patch-filejournal.sql (working copy)
678
679Property changes on: maintenance/archives/patch-filejournal.sql
680___________________________________________________________________
681Added: svn:eol-style
682## -0,0 +1 ##
683+native
684Index: maintenance/tables.sql
685===================================================================
686--- maintenance/tables.sql (revision 113170)
687+++ maintenance/tables.sql (working copy)
688@@ -1485,4 +1485,30 @@
689 -- Should cover *most* configuration - strings, ints, bools, etc.
690 CREATE INDEX /*i*/cf_name_value ON /*_*/config (cf_name,cf_value(255));
691
692+-- File backend operation journal
693+CREATE TABLE /*_*/filejournal (
694+ -- Unique ID for each file operation
695+ fj_id bigint unsigned NOT NULL PRIMARY KEY auto_increment,
696+ -- UUID of the batch this operation belongs to
697+ fj_batch_uuid varbinary(32) NOT NULL,
698+ -- The registered file backend name
699+ fj_backend varchar(255) NOT NULL,
700+ -- The storage path that was affected (may be internal paths)
701+ fj_path blob NOT NULL,
702+ -- SHA-1 file path hash in base-36
703+ fj_path_sha1 varbinary(32) NOT NULL default '',
704+ -- Primitive operation description
705+ fj_op varchar(16) NOT NULL default '',
706+ -- SHA-1 file content hash in base-36 (old and new may be equal)
707+ fj_old_sha1 varbinary(32) NOT NULL default '',
708+ fj_new_sha1 varbinary(32) NOT NULL default '',
709+ -- Timestamp of the batch operation
710+ fj_timestamp varbinary(14) NOT NULL default ''
711+);
712+
713+CREATE INDEX /*i*/fj_batch_id ON /*_*/filejournal (fj_batch_uuid,fj_id);
714+CREATE INDEX /*i*/fj_path_id ON /*_*/filejournal (fj_path_sha1,fj_id);
715+CREATE INDEX /*i*/fj_new_sha1 ON /*_*/filejournal (fj_new_sha1,fj_id);
716+CREATE INDEX /*i*/fj_timestamp ON /*_*/filejournal (fj_timestamp);
717+
718 -- vim: sw=2 sts=2 et