FileStore.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. <?php
  2. /**
  3. * @todo document (needs one-sentence top-level class description).
  4. */
  5. class FileStore {
  6. const DELETE_ORIGINAL = 1;
  7. /**
  8. * Fetch the FileStore object for a given storage group
  9. */
  10. static function get( $group ) {
  11. global $wgFileStore;
  12. if( isset( $wgFileStore[$group] ) ) {
  13. $info = $wgFileStore[$group];
  14. return new FileStore( $group,
  15. $info['directory'],
  16. $info['url'],
  17. intval( $info['hash'] ) );
  18. } else {
  19. return null;
  20. }
  21. }
  22. private function __construct( $group, $directory, $path, $hash ) {
  23. $this->mGroup = $group;
  24. $this->mDirectory = $directory;
  25. $this->mPath = $path;
  26. $this->mHashLevel = $hash;
  27. }
  28. /**
  29. * Acquire a lock; use when performing write operations on a store.
  30. * This is attached to your master database connection, so if you
  31. * suffer an uncaught error the lock will be released when the
  32. * connection is closed.
  33. * @see Database::lock()
  34. */
  35. static function lock() {
  36. $dbw = wfGetDB( DB_MASTER );
  37. $lockname = $dbw->addQuotes( FileStore::lockName() );
  38. return $dbw->lock( $lockname, __METHOD__ );
  39. }
  40. /**
  41. * Release the global file store lock.
  42. * @see Database::unlock()
  43. */
  44. static function unlock() {
  45. $dbw = wfGetDB( DB_MASTER );
  46. $lockname = $dbw->addQuotes( FileStore::lockName() );
  47. return $dbw->unlock( $lockname, __METHOD__ );
  48. }
  49. private static function lockName() {
  50. return 'MediaWiki.' . wfWikiID() . '.FileStore';
  51. }
  52. /**
  53. * Copy a file into the file store from elsewhere in the filesystem.
  54. * Should be protected by FileStore::lock() to avoid race conditions.
  55. *
  56. * @param $key storage key string
  57. * @param $flags
  58. * DELETE_ORIGINAL - remove the source file on transaction commit.
  59. *
  60. * @throws FSException if copy can't be completed
  61. * @return FSTransaction
  62. */
  63. function insert( $key, $sourcePath, $flags=0 ) {
  64. $destPath = $this->filePath( $key );
  65. return $this->copyFile( $sourcePath, $destPath, $flags );
  66. }
  67. /**
  68. * Copy a file from the file store to elsewhere in the filesystem.
  69. * Should be protected by FileStore::lock() to avoid race conditions.
  70. *
  71. * @param $key storage key string
  72. * @param $flags
  73. * DELETE_ORIGINAL - remove the source file on transaction commit.
  74. *
  75. * @throws FSException if copy can't be completed
  76. * @return FSTransaction on success
  77. */
  78. function export( $key, $destPath, $flags=0 ) {
  79. $sourcePath = $this->filePath( $key );
  80. return $this->copyFile( $sourcePath, $destPath, $flags );
  81. }
  82. private function copyFile( $sourcePath, $destPath, $flags=0 ) {
  83. if( !file_exists( $sourcePath ) ) {
  84. // Abort! Abort!
  85. throw new FSException( "missing source file '$sourcePath'" );
  86. }
  87. $transaction = new FSTransaction();
  88. if( $flags & self::DELETE_ORIGINAL ) {
  89. $transaction->addCommit( FSTransaction::DELETE_FILE, $sourcePath );
  90. }
  91. if( file_exists( $destPath ) ) {
  92. // An identical file is already present; no need to copy.
  93. } else {
  94. if( !file_exists( dirname( $destPath ) ) ) {
  95. wfSuppressWarnings();
  96. $ok = wfMkdirParents( dirname( $destPath ) );
  97. wfRestoreWarnings();
  98. if( !$ok ) {
  99. throw new FSException(
  100. "failed to create directory for '$destPath'" );
  101. }
  102. }
  103. wfSuppressWarnings();
  104. $ok = copy( $sourcePath, $destPath );
  105. wfRestoreWarnings();
  106. if( $ok ) {
  107. wfDebug( __METHOD__." copied '$sourcePath' to '$destPath'\n" );
  108. $transaction->addRollback( FSTransaction::DELETE_FILE, $destPath );
  109. } else {
  110. throw new FSException(
  111. __METHOD__." failed to copy '$sourcePath' to '$destPath'" );
  112. }
  113. }
  114. return $transaction;
  115. }
  116. /**
  117. * Delete a file from the file store.
  118. * Caller's responsibility to make sure it's not being used by another row.
  119. *
  120. * File is not actually removed until transaction commit.
  121. * Should be protected by FileStore::lock() to avoid race conditions.
  122. *
  123. * @param $key storage key string
  124. * @throws FSException if file can't be deleted
  125. * @return FSTransaction
  126. */
  127. function delete( $key ) {
  128. $destPath = $this->filePath( $key );
  129. if( false === $destPath ) {
  130. throw new FSException( "file store does not contain file '$key'" );
  131. } else {
  132. return FileStore::deleteFile( $destPath );
  133. }
  134. }
  135. /**
  136. * Delete a non-managed file on a transactional basis.
  137. *
  138. * File is not actually removed until transaction commit.
  139. * Should be protected by FileStore::lock() to avoid race conditions.
  140. *
  141. * @param $path file to remove
  142. * @throws FSException if file can't be deleted
  143. * @return FSTransaction
  144. *
  145. * @todo Might be worth preliminary permissions check
  146. */
  147. static function deleteFile( $path ) {
  148. if( file_exists( $path ) ) {
  149. $transaction = new FSTransaction();
  150. $transaction->addCommit( FSTransaction::DELETE_FILE, $path );
  151. return $transaction;
  152. } else {
  153. throw new FSException( "cannot delete missing file '$path'" );
  154. }
  155. }
  156. /**
  157. * Stream a contained file directly to HTTP output.
  158. * Will throw a 404 if file is missing; 400 if invalid key.
  159. * @return true on success, false on failure
  160. */
  161. function stream( $key ) {
  162. $path = $this->filePath( $key );
  163. if( $path === false ) {
  164. wfHttpError( 400, "Bad request", "Invalid or badly-formed filename." );
  165. return false;
  166. }
  167. if( file_exists( $path ) ) {
  168. // Set the filename for more convenient save behavior from browsers
  169. // FIXME: Is this safe?
  170. header( 'Content-Disposition: inline; filename="' . $key . '"' );
  171. require_once 'StreamFile.php';
  172. wfStreamFile( $path );
  173. } else {
  174. return wfHttpError( 404, "Not found",
  175. "The requested resource does not exist." );
  176. }
  177. }
  178. /**
  179. * Confirm that the given file key is valid.
  180. * Note that a valid key may refer to a file that does not exist.
  181. *
  182. * Key should consist of a 31-digit base-36 SHA-1 hash and
  183. * an optional alphanumeric extension, all lowercase.
  184. * The whole must not exceed 64 characters.
  185. *
  186. * @param $key
  187. * @return boolean
  188. */
  189. static function validKey( $key ) {
  190. return preg_match( '/^[0-9a-z]{31,32}(\.[0-9a-z]{1,31})?$/', $key );
  191. }
  192. /**
  193. * Calculate file storage key from a file on disk.
  194. * You must pass an extension to it, as some files may be calculated
  195. * out of a temporary file etc.
  196. *
  197. * @param $path to file
  198. * @param $extension
  199. * @return string or false if could not open file or bad extension
  200. */
  201. static function calculateKey( $path, $extension ) {
  202. wfSuppressWarnings();
  203. $hash = sha1_file( $path );
  204. wfRestoreWarnings();
  205. if( $hash === false ) {
  206. wfDebug( __METHOD__.": couldn't hash file '$path'\n" );
  207. return false;
  208. }
  209. $base36 = wfBaseConvert( $hash, 16, 36, 31 );
  210. if( $extension == '' ) {
  211. $key = $base36;
  212. } else {
  213. $key = $base36 . '.' . $extension;
  214. }
  215. // Sanity check
  216. if( self::validKey( $key ) ) {
  217. return $key;
  218. } else {
  219. wfDebug( __METHOD__.": generated bad key '$key'\n" );
  220. return false;
  221. }
  222. }
  223. /**
  224. * Return filesystem path to the given file.
  225. * Note that the file may or may not exist.
  226. * @return string or false if an invalid key
  227. */
  228. function filePath( $key ) {
  229. if( self::validKey( $key ) ) {
  230. return $this->mDirectory . DIRECTORY_SEPARATOR .
  231. $this->hashPath( $key, DIRECTORY_SEPARATOR );
  232. } else {
  233. return false;
  234. }
  235. }
  236. /**
  237. * Return URL path to the given file, if the store is public.
  238. * @return string or false if not public
  239. */
  240. function urlPath( $key ) {
  241. if( $this->mUrl && self::validKey( $key ) ) {
  242. return $this->mUrl . '/' . $this->hashPath( $key, '/' );
  243. } else {
  244. return false;
  245. }
  246. }
  247. private function hashPath( $key, $separator ) {
  248. $parts = array();
  249. for( $i = 0; $i < $this->mHashLevel; $i++ ) {
  250. $parts[] = $key{$i};
  251. }
  252. $parts[] = $key;
  253. return implode( $separator, $parts );
  254. }
  255. }
  256. /**
  257. * Wrapper for file store transaction stuff.
  258. *
  259. * FileStore methods may return one of these for undoable operations;
  260. * you can then call its rollback() or commit() methods to perform
  261. * final cleanup if dependent database work fails or succeeds.
  262. */
  263. class FSTransaction {
  264. const DELETE_FILE = 1;
  265. /**
  266. * Combine more items into a fancier transaction
  267. */
  268. function add( FSTransaction $transaction ) {
  269. $this->mOnCommit = array_merge(
  270. $this->mOnCommit, $transaction->mOnCommit );
  271. $this->mOnRollback = array_merge(
  272. $this->mOnRollback, $transaction->mOnRollback );
  273. }
  274. /**
  275. * Perform final actions for success.
  276. * @return true if actions applied ok, false if errors
  277. */
  278. function commit() {
  279. return $this->apply( $this->mOnCommit );
  280. }
  281. /**
  282. * Perform final actions for failure.
  283. * @return true if actions applied ok, false if errors
  284. */
  285. function rollback() {
  286. return $this->apply( $this->mOnRollback );
  287. }
  288. // --- Private and friend functions below...
  289. function __construct() {
  290. $this->mOnCommit = array();
  291. $this->mOnRollback = array();
  292. }
  293. function addCommit( $action, $path ) {
  294. $this->mOnCommit[] = array( $action, $path );
  295. }
  296. function addRollback( $action, $path ) {
  297. $this->mOnRollback[] = array( $action, $path );
  298. }
  299. private function apply( $actions ) {
  300. $result = true;
  301. foreach( $actions as $item ) {
  302. list( $action, $path ) = $item;
  303. if( $action == self::DELETE_FILE ) {
  304. wfSuppressWarnings();
  305. $ok = unlink( $path );
  306. wfRestoreWarnings();
  307. if( $ok )
  308. wfDebug( __METHOD__.": deleting file '$path'\n" );
  309. else
  310. wfDebug( __METHOD__.": failed to delete file '$path'\n" );
  311. $result = $result && $ok;
  312. }
  313. }
  314. return $result;
  315. }
  316. }
  317. /**
  318. * @ingroup Exception
  319. */
  320. class FSException extends MWException { }