Index: branches/FileBackend/phase3/includes/filerepo/backend/FileBackendMultiWrite.php |
— | — | @@ -12,9 +12,11 @@ |
13 | 13 | * The order that the backends are defined sets the priority of which |
14 | 14 | * backend is read from or written to first. Functions like fileExists() |
15 | 15 | * and getFileProps() will return information based on the first backend |
16 | | - * that has the file (normally both should have it anyway). Functions like |
17 | | - * getFileList() will return results from the first backend that is not |
18 | | - * declared as non-persistent cache. This is done for consistency. |
| 16 | + * that has the file (normally both should have it anyway). Special cases: |
| 17 | + * a) getFileList() will return results from the first backend that is |
| 18 | + * not declared as non-persistent cache. This is for correctness. |
| 19 | + * b) getFileHash() will always check only the master backend to keep the |
| 20 | + * result format consistent. |
19 | 21 | * |
20 | 22 | * All write operations are performed on all backends. |
21 | 23 | * If an operation fails on one backend it will be rolled back from the others. |
— | — | @@ -32,20 +34,32 @@ |
33 | 35 | * 'lockManger' : FileLockManager instance |
34 | 36 | * 'backends' : Array of (backend object, settings) pairs. |
35 | 37 | * The settings per backend include: |
36 | | - * 'isCache': The backend is non-persistent |
| 38 | + * 'isCache' : The backend is non-persistent |
| 39 | + * 'isMaster': This must be set for one non-persistent backend. |
37 | 40 | * @param $config Array |
38 | 41 | */ |
39 | 42 | public function __construct( array $config ) { |
40 | 43 | $this->name = $config['name']; |
41 | 44 | $this->lockManager = $config['lockManger']; |
| 45 | + |
| 46 | + $hasMaster = false; |
42 | 47 | foreach ( $config['backends'] as $index => $info ) { |
43 | 48 | list( $backend, $settings ) = $info; |
44 | 49 | $this->fileBackends[$index] = $backend; |
45 | 50 | // Default backend settings |
46 | | - $defaults = array( 'isCache' => false ); |
| 51 | + $defaults = array( 'isCache' => false, 'isMaster' => false ); |
47 | 52 | // Apply custom backend settings to defaults |
48 | 53 | $this->fileBackendsInfo[$index] = $info + $defaults; |
| 54 | + if ( $info['isMaster'] ) { |
| 55 | + if ( $hasMaster ) { |
| 56 | + throw new MWException( 'More than one master backend defined.' ); |
| 57 | + } |
| 58 | + $hasMaster = true; |
| 59 | + } |
49 | 60 | } |
| 61 | + if ( !$hasMaster ) { |
| 62 | + throw new MWException( 'No master backend defined.' ); |
| 63 | + } |
50 | 64 | } |
51 | 65 | |
52 | 66 | final public function doOperations( array $ops ) { |
— | — | @@ -74,6 +88,15 @@ |
75 | 89 | return $status; // abort |
76 | 90 | } |
77 | 91 | |
| 92 | + // Do pre-checks for each operation; abort on failure... |
| 93 | + foreach ( $performOps as $index => $fileOp ) { |
| 94 | + $status->merge( $fileOp->precheck() ); |
| 95 | + if ( !$status->isOK() ) { // operation failed? |
| 96 | + $status->merge( $this->unlockFiles( $filesToLock ) ); |
| 97 | + return $status; |
| 98 | + } |
| 99 | + } |
| 100 | + |
78 | 101 | // Attempt each operation; abort on failure... |
79 | 102 | foreach ( $performOps as $index => $fileOp ) { |
80 | 103 | $status->merge( $fileOp->attempt() ); |
— | — | @@ -85,6 +108,7 @@ |
86 | 109 | $status->merge( $performOps[$pos]->revert() ); |
87 | 110 | $pos--; |
88 | 111 | } |
| 112 | + $status->merge( $this->unlockFiles( $filesToLock ) ); |
89 | 113 | return $status; |
90 | 114 | } |
91 | 115 | } |
— | — | @@ -137,6 +161,14 @@ |
138 | 162 | return $this->doOperation( array( $op ) ); |
139 | 163 | } |
140 | 164 | |
| 165 | + function prepare( array $params ) { |
| 166 | + $status = Status::newGood(); |
| 167 | + foreach ( $this->backends as $backend ) { |
| 168 | + $status->merge( $backend->prepare( $params ) ); |
| 169 | + } |
| 170 | + return $status; |
| 171 | + } |
| 172 | + |
141 | 173 | function fileExists( array $params ) { |
142 | 174 | foreach ( $this->backends as $backend ) { |
143 | 175 | if ( $backend->fileExists( $params ) ) { |
— | — | @@ -145,7 +177,27 @@ |
146 | 178 | } |
147 | 179 | return false; |
148 | 180 | } |
149 | | - |
| 181 | + |
| 182 | + function getFileHash( array $params ) { |
| 183 | + foreach ( $this->backends as $backend ) { |
| 184 | + // Skip non-master for consistent hash formats |
| 185 | + if ( $this->fileBackendsInfo[$index]['isMaster'] ) { |
| 186 | + return $backend->getFileHash( $params ); |
| 187 | + } |
| 188 | + } |
| 189 | + return false; |
| 190 | + } |
| 191 | + |
| 192 | + function getHashType() { |
| 193 | + foreach ( $this->backends as $backend ) { |
| 194 | + // Skip non-master for consistent hash formats |
| 195 | + if ( $this->fileBackendsInfo[$index]['isMaster'] ) { |
| 196 | + return $backend->getHashType(); |
| 197 | + } |
| 198 | + } |
| 199 | + return null; // shouldn't happen |
| 200 | + } |
| 201 | + |
150 | 202 | function getFileProps( array $params ) { |
151 | 203 | foreach ( $this->backends as $backend ) { |
152 | 204 | $props = $backend->getFileProps( $params ); |
Index: branches/FileBackend/phase3/includes/filerepo/backend/FileOp.php |
— | — | @@ -18,11 +18,13 @@ |
19 | 19 | protected $backend; |
20 | 20 | |
21 | 21 | protected $state; |
22 | | - protected $failedAttempt; |
| 22 | + protected $failed; |
23 | 23 | |
| 24 | + /* Object life-cycle */ |
24 | 25 | const STATE_NEW = 1; |
25 | | - const STATE_ATTEMPTED = 2; |
26 | | - const STATE_DONE = 3; |
| 26 | + const STATE_CHECKED = 2; |
| 27 | + const STATE_ATTEMPTED = 3; |
| 28 | + const STATE_DONE = 4; |
27 | 29 | |
28 | 30 | /** |
29 | 31 | * Build a new file operation transaction |
— | — | @@ -34,23 +36,42 @@ |
35 | 37 | $this->backend = $backend; |
36 | 38 | $this->params = $params; |
37 | 39 | $this->state = self::STATE_NEW; |
38 | | - $this->failedAttempt = false; |
| 40 | + $this->failed = false; |
39 | 41 | $this->initialize(); |
40 | 42 | } |
41 | 43 | |
42 | 44 | /** |
| 45 | + * Check preconditions of the operation and possibly stash temp files |
| 46 | + * |
| 47 | + * @return Status |
| 48 | + */ |
| 49 | + final public function precheck() { |
| 50 | + if ( $this->state !== self::STATE_NEW ) { |
| 51 | + return Status::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state ); |
| 52 | + } |
| 53 | + $this->state = self::STATE_CHECKED; |
| 54 | + $status = $this->doPrecheck(); |
| 55 | + if ( !$status->isOK() ) { |
| 56 | + $this->failed = true; |
| 57 | + } |
| 58 | + return $status; |
| 59 | + } |
| 60 | + |
| 61 | + /** |
43 | 62 | * Attempt the operation; this must be reversible |
44 | 63 | * |
45 | 64 | * @return Status |
46 | 65 | */ |
47 | 66 | final public function attempt() { |
48 | | - if ( $this->state !== self::STATE_NEW ) { |
49 | | - throw new MWException( "Cannot attempt operation called twice." ); |
| 67 | + if ( $this->state !== self::STATE_CHECKED ) { |
| 68 | + return Status::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state ); |
| 69 | + } elseif ( $this->failed ) { // failed precheck |
| 70 | + return Status::newFatal( 'fileop-fail-attempt-precheck' ); |
50 | 71 | } |
51 | 72 | $this->state = self::STATE_ATTEMPTED; |
52 | 73 | $status = $this->doAttempt(); |
53 | 74 | if ( !$status->isOK() ) { |
54 | | - $this->failedAttempt = true; |
| 75 | + $this->failed = true; |
55 | 76 | } |
56 | 77 | return $status; |
57 | 78 | } |
— | — | @@ -62,10 +83,10 @@ |
63 | 84 | */ |
64 | 85 | final public function revert() { |
65 | 86 | if ( $this->state !== self::STATE_ATTEMPTED ) { |
66 | | - throw new MWException( "Cannot rollback an unstarted or finished operation." ); |
| 87 | + return Status::newFatal( 'fileop-fail-state', self::STATE_ATTEMPTED, $this->state ); |
67 | 88 | } |
68 | 89 | $this->state = self::STATE_DONE; |
69 | | - if ( $this->failedAttempt ) { |
| 90 | + if ( $this->failed ) { |
70 | 91 | $status = Status::newGood(); // nothing to revert |
71 | 92 | } else { |
72 | 93 | $status = $this->doRevert(); |
— | — | @@ -80,11 +101,11 @@ |
81 | 102 | */ |
82 | 103 | final public function finish() { |
83 | 104 | if ( $this->state !== self::STATE_ATTEMPTED ) { |
84 | | - throw new MWException( "Cannot cleanup an unstarted or finished operation." ); |
| 105 | + return Status::newFatal( 'fileop-fail-state', self::STATE_ATTEMPTED, $this->state ); |
85 | 106 | } |
86 | 107 | $this->state = self::STATE_DONE; |
87 | | - if ( $this->failedAttempt ) { |
88 | | - $status = Status::newGood(); // nothing to revert |
| 108 | + if ( $this->failed ) { |
| 109 | + $status = Status::newGood(); // nothing to finish |
89 | 110 | } else { |
90 | 111 | $status = $this->doFinish(); |
91 | 112 | } |
— | — | @@ -108,6 +129,11 @@ |
109 | 130 | /** |
110 | 131 | * @return Status |
111 | 132 | */ |
| 133 | + abstract protected function doPrecheck(); |
| 134 | + |
| 135 | + /** |
| 136 | + * @return Status |
| 137 | + */ |
112 | 138 | abstract protected function doAttempt(); |
113 | 139 | |
114 | 140 | /** |
— | — | @@ -119,26 +145,138 @@ |
120 | 146 | * @return Status |
121 | 147 | */ |
122 | 148 | abstract protected function doFinish(); |
| 149 | + |
| 150 | + /** |
| 151 | + * Backup any file at the destination to a temporary file. |
| 152 | + * Don't bother backing it up unless we might overwrite the file. |
| 153 | + * This assumes that the destination is in the backend and that |
| 154 | + * the source is either in the backend or on the file system. |
| 155 | + * |
| 156 | + * @return Status |
| 157 | + */ |
| 158 | + protected function checkAndBackupDest() { |
| 159 | + $status = Status::newGood(); |
| 160 | + // Check if a file already exists at the destination |
| 161 | + if ( !$this->backend->fileExists( $this->params['dest'] ) ) { |
| 162 | + return $status; // nothing to do |
| 163 | + } |
| 164 | + |
| 165 | + if ( !empty( $this->params['overwriteDest'] ) ) { |
| 166 | + // Create a temporary backup copy... |
| 167 | + $this->tmpDestFile = $this->getLocalCopy( $this->params['dest'] ); |
| 168 | + if ( !$this->tmpDestFile ) { |
| 169 | + $status->fatal( 'backend-fail-backup', $this->params['dest'] ); |
| 170 | + return $status; |
| 171 | + } |
| 172 | + } elseif ( !empty( $this->params['overwriteSame'] ) ) { |
| 173 | + // Get the source content hash (if there is a single source) |
| 174 | + $shash = $this->getSourceMD5(); |
| 175 | + // If there is a single source, then we can do some checks already. |
| 176 | + // For things like concatenate(), we need to build a temp file first. |
| 177 | + if ( $shash !== null ) { |
| 178 | + $dhash = $this->getFileMD5( $this->params['dest'] ); |
| 179 | + if ( !strlen( $shash ) || !strlen( $dhash ) ) { |
| 180 | + $status->fatal( 'backend-fail-hashes' ); |
| 181 | + return $status; |
| 182 | + } |
| 183 | + // Give an error if the files are not identical |
| 184 | + if ( $shash !== $dhash ) { |
| 185 | + $status->fatal( 'backend-fail-notsame', |
| 186 | + $this->params['source'], $this->params['dest'] ); |
| 187 | + } |
| 188 | + return $status; // do nothing; either OK or bad status |
| 189 | + } |
| 190 | + } else { |
| 191 | + $status->fatal( 'backend-fail-alreadyexists', $params['dest'] ); |
| 192 | + return $status; |
| 193 | + } |
| 194 | + |
| 195 | + return $status; |
| 196 | + } |
| 197 | + |
| 198 | + /** |
| 199 | + * checkAndBackupDest() helper function to get the source file MD5. |
| 200 | + * Returns false on failure and null if there is no single source. |
| 201 | + * |
| 202 | + * @return string|false|null |
| 203 | + */ |
| 204 | + protected function getSourceMD5() { |
| 205 | + return null; // N/A |
| 206 | + } |
| 207 | + |
| 208 | + /** |
| 209 | + * checkAndBackupDest() helper function to get the MD5 of a file. |
| 210 | + * |
| 211 | + * @return string|false False on failure |
| 212 | + */ |
| 213 | + final protected function getFileMD5( $path ) { |
| 214 | + // Source file is in backend |
| 215 | + if ( FileBackend::isStoragePath( $path ) ) { |
| 216 | + // For some backends (e.g. Swift, Azure) we can get |
| 217 | + // standard hashes to use for this types of comparisons. |
| 218 | + if ( $this->backend->getHashType() === 'md5' ) { |
| 219 | + $hash = $this->backend->getFileHash( $path ); |
| 220 | + } else { |
| 221 | + $tmp = $this->getLocalCopy( $path ); |
| 222 | + if ( !$tmp ) { |
| 223 | + return false; // error |
| 224 | + } |
| 225 | + $hash = md5_file( $tmp->getPath() ); |
| 226 | + } |
| 227 | + // Source file is on disk (FS) |
| 228 | + } else { |
| 229 | + $hash = md5_file( $path ); |
| 230 | + } |
| 231 | + return $hash; |
| 232 | + } |
| 233 | + |
| 234 | + /** |
| 235 | + * Restore any temporary destination backup file |
| 236 | + * |
| 237 | + * @return Status |
| 238 | + */ |
| 239 | + protected function restoreDest() { |
| 240 | + $status = Status::newGood(); |
| 241 | + // Restore any file that was at the destination |
| 242 | + if ( $this->tmpDestFile ) { |
| 243 | + $params = array( |
| 244 | + 'source' => $this->tmpDestFile->getPath(), |
| 245 | + 'dest' => $this->params['dest'] |
| 246 | + ); |
| 247 | + $status = $this->backend->store( $params ); |
| 248 | + if ( !$status->isOK() ) { |
| 249 | + return $status; |
| 250 | + } |
| 251 | + } |
| 252 | + return $status; |
| 253 | + } |
123 | 254 | } |
124 | 255 | |
125 | 256 | /** |
126 | 257 | * Store a file into the backend from a file on disk. |
127 | | - * Parameters must match FileBackend::store(), which include: |
128 | | - * source : source path on disk |
| 258 | + * Parameters similar to FileBackend::store(), which include: |
| 259 | + * source : source path on disk (FS) |
129 | 260 | * dest : destination storage path |
130 | 261 | * overwriteDest : do nothing and pass if an identical file exists at destination |
131 | 262 | * overwriteSame : override any existing file at destination |
132 | 263 | */ |
133 | | -class FileStoreOp extends FileOp { |
| 264 | +class StoreFileOp extends FileOp { |
134 | 265 | /** @var TempLocalFile|null */ |
135 | 266 | protected $tmpDestFile; // temp copy of existing destination file |
136 | 267 | |
137 | | - function doAttempt() { |
138 | | - // Create a backup copy of any file that exists at destination |
139 | | - $status = $this->backupDest(); |
140 | | - if ( !$status->isOK() ) { |
| 268 | + function doPrecheck() { |
| 269 | + $status = Status::newGood(); |
| 270 | + // Check if the source files exists on disk (FS) |
| 271 | + if ( !file_exists( $this->params['source'] ) ) { |
| 272 | + $status->fatal( 'backend-fail-notexists', $this->params['source'] ); |
141 | 273 | return $status; |
142 | 274 | } |
| 275 | + // Create a destination backup copy as needed |
| 276 | + $status->merge( $this->checkAndBackupDest() ); |
| 277 | + return $status; |
| 278 | + } |
| 279 | + |
| 280 | + function doAttempt() { |
143 | 281 | // Store the file at the destination |
144 | 282 | $status = $this->backend->store( $this->params ); |
145 | 283 | return $status; |
— | — | @@ -164,65 +302,27 @@ |
165 | 303 | return array( $this->params['dest'] ); |
166 | 304 | } |
167 | 305 | |
168 | | - /** |
169 | | - * Backup any file at destination to a temporary file. |
170 | | - * Don't bother backing it up unless we might overwrite the file. |
171 | | - * |
172 | | - * @return Status |
173 | | - */ |
174 | | - protected function backupDest() { |
175 | | - $status = Status::newGood(); |
176 | | - // Check if a file already exists at the destination... |
177 | | - if ( $this->backend->fileExists( $this->params['dest'] ) ) { |
178 | | - if ( $this->params['overwriteDest'] ) { |
179 | | - // Create a temporary backup copy... |
180 | | - $this->tmpDestFile = $this->getLocalCopy( $this->params['dest'] ); |
181 | | - if ( !$this->tmpDestFile ) { |
182 | | - $status->fatal( 'backend-fail-restore', $this->params['dest'] ); |
183 | | - return $status; |
184 | | - } |
185 | | - } |
186 | | - } |
187 | | - return $status; |
| 306 | + function getSourceMD5() { |
| 307 | + return md5_file( $this->params['source'] ); |
188 | 308 | } |
189 | | - |
190 | | - /** |
191 | | - * Restore any temporary destination backup file |
192 | | - * |
193 | | - * @return Status |
194 | | - */ |
195 | | - protected function restoreDest() { |
196 | | - $status = Status::newGood(); |
197 | | - // Restore any file that was at the destination |
198 | | - if ( $this->tmpDestFile ) { |
199 | | - $params = array( |
200 | | - 'source' => $this->tmpDestFile->getPath(), |
201 | | - 'dest' => $this->params['dest'] |
202 | | - ); |
203 | | - $status = $this->backend->store( $params ); |
204 | | - if ( !$status->isOK() ) { |
205 | | - return $status; |
206 | | - } |
207 | | - } |
208 | | - return $status; |
209 | | - } |
210 | 309 | } |
211 | 310 | |
212 | 311 | /** |
213 | 312 | * Create a file in the backend with the given content. |
214 | | - * Parameters must match FileBackend::create(), which include: |
| 313 | + * Parameters similar to FileBackend::create(), which include: |
215 | 314 | * content : a string of raw file contents |
216 | 315 | * dest : destination storage path |
217 | 316 | * overwriteDest : do nothing and pass if an identical file exists at destination |
218 | 317 | * overwriteSame : override any existing file at destination |
219 | 318 | */ |
220 | | -class FileCreateOp extends FileStoreOp { |
| 319 | +class CreateFileOp extends FileOp { |
| 320 | + function doPrecheck() { |
| 321 | + // Create a destination backup copy as needed |
| 322 | + $status = $this->checkAndBackupDest(); |
| 323 | + return $status; |
| 324 | + } |
| 325 | + |
221 | 326 | function doAttempt() { |
222 | | - // Create a backup copy of any file that exists at destination |
223 | | - $status = $this->backupDest(); |
224 | | - if ( !$status->isOK() ) { |
225 | | - return $status; |
226 | | - } |
227 | 327 | // Create the file at the destination |
228 | 328 | $status = $this->backend->create( $this->params ); |
229 | 329 | return $status; |
— | — | @@ -247,23 +347,35 @@ |
248 | 348 | function storagePathsToLock() { |
249 | 349 | return array( $this->params['dest'] ); |
250 | 350 | } |
| 351 | + |
| 352 | + function getSourceMD5() { |
| 353 | + return md5( $this->params['content'] ); |
| 354 | + } |
251 | 355 | } |
252 | 356 | |
253 | 357 | /** |
254 | 358 | * Copy a file from one storage path to another in the backend. |
255 | | - * Parameters must match FileBackend::copy(), which include: |
| 359 | + * Parameters similar to FileBackend::copy(), which include: |
256 | 360 | * source : source storage path |
257 | 361 | * dest : destination storage path |
258 | 362 | * overwriteDest : do nothing and pass if an identical file exists at destination |
259 | 363 | * overwriteSame : override any existing file at destination |
260 | 364 | */ |
261 | | -class FileCopyOp extends FileStoreOp { |
262 | | - function doAttempt() { |
263 | | - // Create a backup copy of any file that exists at destination |
264 | | - $status = $this->backupDest(); |
265 | | - if ( !$status->isOK() ) { |
| 365 | +class CopyFileOp extends StoreFileOp { |
| 366 | + function doPrecheck() { |
| 367 | + $status = Status::newGood(); |
| 368 | + // Check if the source files exists on disk |
| 369 | + $params = array( 'source' => $this->params['source'] ); |
| 370 | + if ( !$this->backend->fileExists( $params ) ) { |
| 371 | + $status->fatal( 'backend-fail-notexists', $this->params['source'] ); |
266 | 372 | return $status; |
267 | 373 | } |
| 374 | + // Create a destination backup copy as needed |
| 375 | + $status->merge( $this->checkAndBackupDest() ); |
| 376 | + return $status; |
| 377 | + } |
| 378 | + |
| 379 | + function doAttempt() { |
268 | 380 | // Copy the file into the destination |
269 | 381 | $status = $this->backend->copy( $this->params ); |
270 | 382 | return $status; |
— | — | @@ -288,17 +400,21 @@ |
289 | 401 | function storagePathsToLock() { |
290 | 402 | return array( $this->params['source'], $this->params['dest'] ); |
291 | 403 | } |
| 404 | + |
| 405 | + function getSourceMD5() { |
| 406 | + return $this->getFileMD5( $this->params['source'] ); |
| 407 | + } |
292 | 408 | } |
293 | 409 | |
294 | 410 | /** |
295 | 411 | * Move a file from one storage path to another in the backend. |
296 | | - * Parameters must match FileBackend::move(), which include: |
| 412 | + * Parameters similar to FileBackend::move(), which include: |
297 | 413 | * source : source storage path |
298 | 414 | * dest : destination storage path |
299 | 415 | * overwriteDest : do nothing and pass if an identical file exists at destination |
300 | 416 | * overwriteSame : override any existing file at destination |
301 | 417 | */ |
302 | | -class FileMoveOp extends FileStoreOp { |
| 418 | +class MoveFileOp extends FileOp { |
303 | 419 | protected $usingMove = false; // using backend move() function? |
304 | 420 | |
305 | 421 | function initialize() { |
— | — | @@ -306,12 +422,20 @@ |
307 | 423 | $this->usingMove = $this->backend->canMove( $this->params ); |
308 | 424 | } |
309 | 425 | |
310 | | - function doAttempt() { |
311 | | - // Create a backup copy of any file that exists at destination |
312 | | - $status = $this->backupDest(); |
313 | | - if ( !$status->isOK() ) { |
| 426 | + function doPrecheck() { |
| 427 | + $status = Status::newGood(); |
| 428 | + // Check if the source files exists on disk |
| 429 | + $params = array( 'source' => $this->params['source'] ); |
| 430 | + if ( !$this->backend->fileExists( $params ) ) { |
| 431 | + $status->fatal( 'backend-fail-notexists', $this->params['source'] ); |
314 | 432 | return $status; |
315 | 433 | } |
| 434 | + // Create a destination backup copy as needed |
| 435 | + $status->merge( $this->checkAndBackupDest() ); |
| 436 | + return $status; |
| 437 | + } |
| 438 | + |
| 439 | + function doAttempt() { |
316 | 440 | // Native moves: move the file into the destination |
317 | 441 | if ( $this->usingMove ) { |
318 | 442 | $status = $this->backend->move( $this->params ); |
— | — | @@ -361,20 +485,30 @@ |
362 | 486 | function storagePathsToLock() { |
363 | 487 | return array( $this->params['source'], $this->params['dest'] ); |
364 | 488 | } |
| 489 | + |
| 490 | + function getSourceMD5() { |
| 491 | + return $this->getFileMD5( $this->params['source'] ); |
| 492 | + } |
365 | 493 | } |
366 | 494 | |
367 | 495 | /** |
368 | 496 | * Combines files from severals storage paths into a new file in the backend. |
369 | | - * Parameters must match FileBackend::concatenate(), which include: |
| 497 | + * Parameters similar to FileBackend::concatenate(), which include: |
370 | 498 | * sources : ordered source storage paths (e.g. chunk1,chunk2,...) |
371 | 499 | * dest : destination storage path |
372 | 500 | * overwriteDest : do nothing and pass if an identical file exists at destination |
373 | 501 | * overwriteSame : override any existing file at destination |
374 | 502 | */ |
375 | | -class FileConcatenateOp extends FileStoreOp { |
| 503 | +class ConcatenateFileOp extends FileOp { |
| 504 | + function doPrecheck() { |
| 505 | + // Create a destination backup copy as needed |
| 506 | + $status = $this->checkAndBackupDest(); |
| 507 | + return $status; |
| 508 | + } |
| 509 | + |
376 | 510 | function doAttempt() { |
377 | 511 | // Create a backup copy of any file that exists at destination |
378 | | - $status = $this->backupDest(); |
| 512 | + $status = $this->checkAndBackupDest(); |
379 | 513 | if ( !$status->isOK() ) { |
380 | 514 | return $status; |
381 | 515 | } |
— | — | @@ -402,19 +536,24 @@ |
403 | 537 | function storagePathsToLock() { |
404 | 538 | return array_merge( $this->params['sources'], $this->params['dest'] ); |
405 | 539 | } |
| 540 | + |
| 541 | + function getSourceMD5() { |
| 542 | + return null; // defer this until we finish building the new file |
| 543 | + } |
406 | 544 | } |
407 | 545 | |
408 | 546 | /** |
409 | 547 | * Delete a file at the storage path. |
410 | | - * Parameters must match FileBackend::delete(), which include: |
| 548 | + * Parameters similar to FileBackend::delete(), which include: |
411 | 549 | * source : source storage path |
412 | 550 | * ignoreMissingSource : don't return an error if the file does not exist |
413 | 551 | */ |
414 | | -class FileDeleteOp extends FileOp { |
415 | | - function doAttempt() { |
| 552 | +class DeleteFileOp extends FileOp { |
| 553 | + function doPrecheck() { |
416 | 554 | $status = Status::newGood(); |
417 | | - if ( !$this->params['ignoreMissingSource'] ) { |
418 | | - if ( !$this->backend->fileExists( $this->params['source'] ) ) { |
| 555 | + if ( empty( $this->params['ignoreMissingSource'] ) ) { |
| 556 | + $params = array( 'source' => $this->params['source'] ); |
| 557 | + if ( !$this->backend->fileExists( $params ) ) { |
419 | 558 | $status->fatal( 'backend-fail-notexists', $this->params['source'] ); |
420 | 559 | return $status; |
421 | 560 | } |
— | — | @@ -422,6 +561,10 @@ |
423 | 562 | return $status; |
424 | 563 | } |
425 | 564 | |
| 565 | + function doAttempt() { |
| 566 | + return Status::newGood(); |
| 567 | + } |
| 568 | + |
426 | 569 | function doRevert() { |
427 | 570 | return Status::newGood(); |
428 | 571 | } |
Index: branches/FileBackend/phase3/includes/filerepo/backend/FSFileBackend.php |
— | — | @@ -305,6 +305,22 @@ |
306 | 306 | return $status; |
307 | 307 | } |
308 | 308 | |
| 309 | + function prepare( array $params ) { |
| 310 | + list( $c, $dir ) = $this->resolveVirtualPath( $params['directory'] ); |
| 311 | + if ( $dir === null ) { |
| 312 | + return false; // invalid storage path |
| 313 | + } |
| 314 | + if ( !wfMkdirParents( $dir ) ) { |
| 315 | + $status->fatal( 'directorycreateerror', $param['directory'] ); |
| 316 | + return $status; |
| 317 | + } |
| 318 | + if ( !is_writable( $dir ) ) { |
| 319 | + $status->fatal( 'directoryreadonlyerror', $param['directory'] ); |
| 320 | + return $status; |
| 321 | + } |
| 322 | + return $status; |
| 323 | + } |
| 324 | + |
309 | 325 | function fileExists( array $params ) { |
310 | 326 | list( $c, $source ) = $this->resolveVirtualPath( $params['source'] ); |
311 | 327 | if ( $source === null ) { |
— | — | @@ -313,6 +329,18 @@ |
314 | 330 | return file_exists( $source ); |
315 | 331 | } |
316 | 332 | |
| 333 | + function getHashType() { |
| 334 | + return 'md5'; |
| 335 | + } |
| 336 | + |
| 337 | + function getFileHash( array $params ) { |
| 338 | + list( $c, $source ) = $this->resolveVirtualPath( $params['source'] ); |
| 339 | + if ( $source === null ) { |
| 340 | + return false; // invalid storage path |
| 341 | + } |
| 342 | + return md5_file( $source ); |
| 343 | + } |
| 344 | + |
317 | 345 | function getFileProps( array $params ) { |
318 | 346 | list( $c, $source ) = $this->resolveVirtualPath( $params['source'] ); |
319 | 347 | if ( $source === null ) { |
Index: branches/FileBackend/phase3/includes/filerepo/backend/FileLockManager.php |
— | — | @@ -67,7 +67,9 @@ |
68 | 68 | * Simple version of FileLockManager based on using FS lock files |
69 | 69 | * |
70 | 70 | * This should work fine for small sites running off one server. |
71 | | - * Do not use this with 'lockDir' set to an NFS mount. |
| 71 | + * Do not use this with 'lockDir' set to an NFS mount unless the |
| 72 | + * NFS client is at least version 2.6.12. Otherwise, the BSD flock() |
| 73 | + * locks will be ignored; see http://nfs.sourceforge.net/#section_d. |
72 | 74 | */ |
73 | 75 | class FSFileLockManager extends FileLockManager { |
74 | 76 | protected $lockDir; // global dir for all servers |
Index: branches/FileBackend/phase3/includes/filerepo/backend/FileBackend.php |
— | — | @@ -88,7 +88,6 @@ |
89 | 89 | * source : source path on disk |
90 | 90 | * dest : destination storage path |
91 | 91 | * overwriteDest : do nothing and pass if an identical file exists at destination |
92 | | - * overwriteSame : override any existing file at destination |
93 | 92 | * |
94 | 93 | * @param Array $params |
95 | 94 | * @return Status |
— | — | @@ -102,7 +101,6 @@ |
103 | 102 | * source : source storage path |
104 | 103 | * dest : destination storage path |
105 | 104 | * overwriteDest : do nothing and pass if an identical file exists at destination |
106 | | - * overwriteSame : override any existing file at destination |
107 | 105 | * |
108 | 106 | * @param Array $params |
109 | 107 | * @return Status |
— | — | @@ -117,7 +115,6 @@ |
118 | 116 | * source : source storage path |
119 | 117 | * dest : destination storage path |
120 | 118 | * overwriteDest : do nothing and pass if an identical file exists at destination |
121 | | - * overwriteSame : override any existing file at destination |
122 | 119 | * |
123 | 120 | * @param Array $params |
124 | 121 | * @return Status |
— | — | @@ -129,7 +126,6 @@ |
130 | 127 | * Do not call this function from places outside FileBackend and FileOp. |
131 | 128 | * $params include: |
132 | 129 | * source : source storage path |
133 | | - * ignoreMissingSource : don't return an error if the file does not exist |
134 | 130 | * |
135 | 131 | * @param Array $params |
136 | 132 | * @return Status |
— | — | @@ -157,7 +153,6 @@ |
158 | 154 | * contents : the raw file contents |
159 | 155 | * dest : destination storage path |
160 | 156 | * overwriteDest : do nothing and pass if an identical file exists at destination |
161 | | - * overwriteSame : override any existing file at destination |
162 | 157 | * |
163 | 158 | * @param Array $params |
164 | 159 | * @return Status |
— | — | @@ -190,6 +185,26 @@ |
191 | 186 | abstract public function fileExists( array $params ); |
192 | 187 | |
193 | 188 | /** |
| 189 | + * Get the format of the hash that getFileHash() uses |
| 190 | + * |
| 191 | + * @return string (md5, sha1, unknown, ...) |
| 192 | + */ |
| 193 | + public function getHashType() { |
| 194 | + return 'unknown'; |
| 195 | + } |
| 196 | + |
| 197 | + /** |
| 198 | + * Get a hash of the file that exists at a storage path in the backend. |
| 199 | + * Typically this will be a SHA-1 hash, MD5 hash, or something similar. |
| 200 | + * $params include: |
| 201 | + * source : source storage path |
| 202 | + * |
| 203 | + * @param Array $params |
| 204 | + * @return string|null Gives null if the file does not exist |
| 205 | + */ |
| 206 | + abstract public function getFileHash( array $params ); |
| 207 | + |
| 208 | + /** |
194 | 209 | * Get the properties of the file that exists at a storage path in the backend |
195 | 210 | * $params include: |
196 | 211 | * source : source storage path |
— | — | @@ -278,12 +293,12 @@ |
279 | 294 | */ |
280 | 295 | protected function supportedOperations() { |
281 | 296 | return array( |
282 | | - 'store' => 'FileStoreOp', |
283 | | - 'copy' => 'FileCopyOp', |
284 | | - 'move' => 'FileMoveOp', |
285 | | - 'delete' => 'FileDeleteOp', |
286 | | - 'concatenate' => 'FileConcatenateOp', |
287 | | - 'create' => 'FileCreateOp' |
| 297 | + 'store' => 'StoreFileOp', |
| 298 | + 'copy' => 'CopyFileOp', |
| 299 | + 'move' => 'MoveFileOp', |
| 300 | + 'delete' => 'DeleteFileOp', |
| 301 | + 'concatenate' => 'ConcatenateFileOp', |
| 302 | + 'create' => 'CreateFileOp' |
288 | 303 | ); |
289 | 304 | } |
290 | 305 | |
— | — | @@ -336,6 +351,15 @@ |
337 | 352 | return $status; // abort |
338 | 353 | } |
339 | 354 | |
| 355 | + // Do pre-checks for each operation; abort on failure... |
| 356 | + foreach ( $performOps as $index => $fileOp ) { |
| 357 | + $status->merge( $fileOp->precheck() ); |
| 358 | + if ( !$status->isOK() ) { // operation failed? |
| 359 | + $status->merge( $this->unlockFiles( $filesToLock ) ); |
| 360 | + return $status; |
| 361 | + } |
| 362 | + } |
| 363 | + |
340 | 364 | // Attempt each operation; abort on failure... |
341 | 365 | foreach ( $performOps as $index => $fileOp ) { |
342 | 366 | $status->merge( $fileOp->attempt() ); |
— | — | @@ -347,6 +371,7 @@ |
348 | 372 | $status->merge( $performOps[$pos]->revert() ); |
349 | 373 | $pos--; |
350 | 374 | } |
| 375 | + $status->merge( $this->unlockFiles( $filesToLock ) ); |
351 | 376 | return $status; |
352 | 377 | } |
353 | 378 | } |
— | — | @@ -366,6 +391,16 @@ |
367 | 392 | } |
368 | 393 | |
369 | 394 | /** |
| 395 | + * Check if a given path is a mwstore:// path |
| 396 | + * |
| 397 | + * @param $path string |
| 398 | + * @return bool |
| 399 | + */ |
| 400 | + final public static function isStoragePath( $path ) { |
| 401 | + return ( strpos( $path, 'mwstore://' ) === 0 ); |
| 402 | + } |
| 403 | + |
| 404 | + /** |
370 | 405 | * Split a storage path (e.g. "mwstore://backend/container/path/to/object") |
371 | 406 | * into a container name and a full object name within that container. |
372 | 407 | * |