Index: trunk/phase3/includes/Status.php |
— | — | @@ -17,7 +17,9 @@ |
18 | 18 | var $value; |
19 | 19 | |
20 | 20 | /** Counters for batch operations */ |
21 | | - var $successCount = 0, $failCount = 0; |
| 21 | + public $successCount = 0, $failCount = 0; |
| 22 | + /** Array to indicate which items of the batch operations failed */ |
| 23 | + public $success = array(); |
22 | 24 | |
23 | 25 | /*semi-private*/ var $errors = array(); |
24 | 26 | /*semi-private*/ var $cleanCallback = false; |
Index: trunk/phase3/includes/filerepo/LocalFile.php |
— | — | @@ -1197,7 +1197,7 @@ |
1198 | 1198 | |
1199 | 1199 | $status = $batch->execute(); |
1200 | 1200 | |
1201 | | - if ( !$status->ok ) { |
| 1201 | + if ( !$status->isGood() ) { |
1202 | 1202 | return $status; |
1203 | 1203 | } |
1204 | 1204 | |
— | — | @@ -1807,9 +1807,11 @@ |
1808 | 1808 | $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME ); |
1809 | 1809 | $status->merge( $storeStatus ); |
1810 | 1810 | |
1811 | | - if ( !$status->ok ) { |
1812 | | - // Store batch returned a critical error -- this usually means nothing was stored |
1813 | | - // Stop now and return an error |
| 1811 | + if ( !$status->isGood() ) { |
| 1812 | + // Even if some files could be copied, fail entirely as that is the |
| 1813 | + // easiest thing to do without data loss |
| 1814 | + $this->cleanupFailedBatch( $storeStatus, $storeBatch ); |
| 1815 | + $status->ok = false; |
1814 | 1816 | $this->file->unlock(); |
1815 | 1817 | |
1816 | 1818 | return $status; |
— | — | @@ -1914,6 +1916,17 @@ |
1915 | 1917 | |
1916 | 1918 | return $status; |
1917 | 1919 | } |
| 1920 | + |
| 1921 | + function cleanupFailedBatch( $storeStatus, $storeBatch ) { |
| 1922 | + $cleanupBatch = array(); |
| 1923 | + |
| 1924 | + foreach ( $storeStatus->success as $i => $success ) { |
| 1925 | + if ( $success ) { |
| 1926 | + $cleanupBatch[] = array( $storeBatch[$i][1], $storeBatch[$i][1] ); |
| 1927 | + } |
| 1928 | + } |
| 1929 | + $this->file->repo->cleanupBatch( $cleanupBatch ); |
| 1930 | + } |
1918 | 1931 | } |
1919 | 1932 | |
1920 | 1933 | # ------------------------------------------------------------------------------ |
Index: trunk/phase3/includes/filerepo/FSRepo.php |
— | — | @@ -146,16 +146,23 @@ |
147 | 147 | * same contents as the source |
148 | 148 | */ |
149 | 149 | function storeBatch( $triplets, $flags = 0 ) { |
| 150 | + wfDebug( __METHOD__ . ': Storing ' . count( $triplets ) . |
| 151 | + " triplets; flags: {$flags}\n" ); |
| 152 | + |
| 153 | + // Try creating directories |
150 | 154 | if ( !wfMkdirParents( $this->directory ) ) { |
151 | 155 | return $this->newFatal( 'upload_directory_missing', $this->directory ); |
152 | 156 | } |
153 | 157 | if ( !is_writable( $this->directory ) ) { |
154 | 158 | return $this->newFatal( 'upload_directory_read_only', $this->directory ); |
155 | 159 | } |
| 160 | + |
| 161 | + // Validate each triplet |
156 | 162 | $status = $this->newGood(); |
157 | 163 | foreach ( $triplets as $i => $triplet ) { |
158 | 164 | list( $srcPath, $dstZone, $dstRel ) = $triplet; |
159 | 165 | |
| 166 | + // Resolve destination path |
160 | 167 | $root = $this->getZonePath( $dstZone ); |
161 | 168 | if ( !$root ) { |
162 | 169 | throw new MWException( "Invalid zone: $dstZone" ); |
— | — | @@ -166,6 +173,7 @@ |
167 | 174 | $dstPath = "$root/$dstRel"; |
168 | 175 | $dstDir = dirname( $dstPath ); |
169 | 176 | |
| 177 | + // Create destination directories for this triplet |
170 | 178 | if ( !is_dir( $dstDir ) ) { |
171 | 179 | if ( !wfMkdirParents( $dstDir ) ) { |
172 | 180 | return $this->newFatal( 'directorycreateerror', $dstDir ); |
— | — | @@ -175,6 +183,7 @@ |
176 | 184 | } |
177 | 185 | } |
178 | 186 | |
| 187 | + // Resolve source |
179 | 188 | if ( self::isVirtualUrl( $srcPath ) ) { |
180 | 189 | $srcPath = $triplets[$i][0] = $this->resolveVirtualUrl( $srcPath ); |
181 | 190 | } |
— | — | @@ -183,6 +192,8 @@ |
184 | 193 | $status->fatal( 'filenotfound', $srcPath ); |
185 | 194 | continue; |
186 | 195 | } |
| 196 | + |
| 197 | + // Check overwriting |
187 | 198 | if ( !( $flags & self::OVERWRITE ) && file_exists( $dstPath ) ) { |
188 | 199 | if ( $flags & self::OVERWRITE_SAME ) { |
189 | 200 | $hashSource = sha1_file( $srcPath ); |
— | — | @@ -196,6 +207,7 @@ |
197 | 208 | } |
198 | 209 | } |
199 | 210 | |
| 211 | + // Windows does not support moving over existing files, so explicitly delete them |
200 | 212 | $deleteDest = wfIsWindows() && ( $flags & self::OVERWRITE ); |
201 | 213 | |
202 | 214 | // Abort now on failure |
— | — | @@ -203,7 +215,8 @@ |
204 | 216 | return $status; |
205 | 217 | } |
206 | 218 | |
207 | | - foreach ( $triplets as $triplet ) { |
| 219 | + // Execute the store operation for each triplet |
| 220 | + foreach ( $triplets as $i => $triplet ) { |
208 | 221 | list( $srcPath, $dstZone, $dstRel ) = $triplet; |
209 | 222 | $root = $this->getZonePath( $dstZone ); |
210 | 223 | $dstPath = "$root/$dstRel"; |
— | — | @@ -222,6 +235,20 @@ |
223 | 236 | $status->error( 'filecopyerror', $srcPath, $dstPath ); |
224 | 237 | $good = false; |
225 | 238 | } |
| 239 | + if ( !( $flags & self::SKIP_VALIDATION ) ) { |
| 240 | + wfSuppressWarnings(); |
| 241 | + $hashSource = sha1_file( $srcPath ); |
| 242 | + $hashDest = sha1_file( $dstPath ); |
| 243 | + wfRestoreWarnings(); |
| 244 | + |
| 245 | + if ( $hashDest === false || $hashSource !== $hashDest ) { |
| 246 | + wfDebug( __METHOD__ . ': File copy validation failed: ' . |
| 247 | + "$srcPath ($hashSource) to $dstPath ($hashDest)\n" ); |
| 248 | + |
| 249 | + $status->error( 'filecopyerror', $srcPath, $dstPath ); |
| 250 | + $good = false; |
| 251 | + } |
| 252 | + } |
226 | 253 | } |
227 | 254 | if ( $good ) { |
228 | 255 | $this->chmod( $dstPath ); |
— | — | @@ -229,9 +256,28 @@ |
230 | 257 | } else { |
231 | 258 | $status->failCount++; |
232 | 259 | } |
| 260 | + $status->success[$i] = $good; |
233 | 261 | } |
234 | 262 | return $status; |
235 | 263 | } |
| 264 | + |
| 265 | + /** |
| 266 | + * Deletes a batch of (zone, rel) pairs. It will try to delete each pair, |
| 267 | + * but ignores any errors doing so. |
| 268 | + * |
| 269 | + * @param $pairs array Pair of (zone, rel) pairs to delete |
| 270 | + */ |
| 271 | + function cleanupBatch( $pairs ) { |
| 272 | + foreach ( $pairs as $i => $pair ) { |
| 273 | + list( $zone, $rel ) = $pair; |
| 274 | + $root = $this->getZonePath( $zone ); |
| 275 | + $path = "$root/$rel"; |
| 276 | + |
| 277 | + wfSuppressWarnings(); |
| 278 | + unlink( $path ); |
| 279 | + wfRestoreWarnings(); |
| 280 | + } |
| 281 | + } |
236 | 282 | |
237 | 283 | function append( $srcPath, $toAppendPath, $flags = 0 ) { |
238 | 284 | $status = $this->newGood(); |
Index: trunk/phase3/includes/filerepo/FileRepo.php |
— | — | @@ -17,6 +17,7 @@ |
18 | 18 | const DELETE_SOURCE = 1; |
19 | 19 | const OVERWRITE = 2; |
20 | 20 | const OVERWRITE_SAME = 4; |
| 21 | + const SKIP_VALIDATION = 8; |
21 | 22 | |
22 | 23 | var $thumbScriptUrl, $transformVia404; |
23 | 24 | var $descBaseUrl, $scriptDirUrl, $scriptExtension, $articleUrl; |
Index: trunk/phase3/includes/specials/SpecialUndelete.php |
— | — | @@ -336,6 +336,9 @@ |
337 | 337 | if( $restoreFiles && $this->title->getNamespace() == NS_FILE ) { |
338 | 338 | $img = wfLocalFile( $this->title ); |
339 | 339 | $this->fileStatus = $img->restore( $fileVersions, $unsuppress ); |
| 340 | + if ( !$this->fileStatus->isOk() ) { |
| 341 | + return false; |
| 342 | + } |
340 | 343 | $filesRestored = $this->fileStatus->successCount; |
341 | 344 | } else { |
342 | 345 | $filesRestored = 0; |
Index: trunk/phase3/RELEASE-NOTES |
— | — | @@ -112,6 +112,7 @@ |
113 | 113 | * (bug 26809) Uploading files with multiple extensions where one of the extensions |
114 | 114 | is blacklisted now gives the proper extension in the error message. |
115 | 115 | * (bug 26961) Hide anon edits in watchlist preference now actually works. |
| 116 | +* (bug 19751) Filesystem is now checked during image undeletion |
116 | 117 | |
117 | 118 | === API changes in 1.18 === |
118 | 119 | * (bug 26339) Throw warning when truncating an overlarge API result |