Add AtomicWriter.class.php
authorTim Düsterhus <duesterhus@woltlab.com>
Mon, 8 Sep 2014 15:53:32 +0000 (17:53 +0200)
committerTim Düsterhus <duesterhus@woltlab.com>
Mon, 8 Sep 2014 16:27:30 +0000 (18:27 +0200)
wcfsetup/install/files/lib/system/io/AtomicWriter.class.php [new file with mode: 0644]

diff --git a/wcfsetup/install/files/lib/system/io/AtomicWriter.class.php b/wcfsetup/install/files/lib/system/io/AtomicWriter.class.php
new file mode 100644 (file)
index 0000000..9fecdac
--- /dev/null
@@ -0,0 +1,119 @@
+<?php
+namespace wcf\system\io;
+use wcf\system\exception\SystemException;
+use wcf\util\FileUtil;
+
+/**
+ * AtomicWriter performs an atomic write operation to the given file.
+ * Nothing is written to the actual file, until you explicitly call 'flush',
+ * simply closing the file will discard any written data.
+ * 
+ * Only a single 'flush' is supported, the AtomicWriter will be unusable after
+ * the data was flushed.
+ * 
+ * Note: The AtomicWriter only supports a small number of whitelisted (write) operations.
+ *
+ * @author     Tim Duesterhus
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage system.io
+ * @category   Community Framework
+ */
+class AtomicWriter extends File {
+       /**
+        * The file the data should be flushed into.
+        * @var string
+        */
+       protected $targetFilename = '';
+       
+       /**
+        * AtomicWriter will be unusable, once this flag is true.
+        * @var boolean
+        */
+       protected $isFlushed = false;
+       
+       /**
+        * Opens a new file. The file is always opened in binary mode.
+        * 
+        * @param       string          $filename
+        */
+       public function __construct($filename) {
+               $this->targetFilename = $filename;
+               
+               $i = 0;
+               while (true) {
+                       try {
+                               parent::__construct(FileUtil::getTemporaryFilename('atomic_'), 'xb');
+                               break;
+                       }
+                       catch (SystemException $e) {
+                               // allow at most 5 failures
+                               if (++$i == 5) {
+                                       throw $e;
+                               }
+                       }
+               }
+               
+               if (!flock($this->resource, LOCK_EX)) throw new SystemException('Could not get lock on temporary file');
+       }
+       
+       /**
+        * @see \wcf\system\io\AtomicWriter::close()
+        */
+       public function __destruct() {
+               $this->close();
+       }
+       
+       /**
+        * Closes the file, while discarding any written data, noop if the
+        * file is already closed or flushed.
+        */
+       public function close() {
+               if (!$this->isFlushed) {
+                       $this->isFlushed = true;
+                       
+                       flock($this->resource, LOCK_UN);
+                       fclose($this->resource);
+                       @unlink($this->filename);
+               }
+       }
+       
+       /**
+        * Persists the written data into the target file. The flush is atomic
+        * if the underlying storage supports an atomic rename.
+        */
+       public function flush() {
+               $this->isFlushed = true;
+               
+               fflush($this->resource);
+               flock($this->resource, LOCK_UN);
+               fclose($this->resource);
+               
+               rename($this->filename, $this->targetFilename);
+       }
+       
+       /**
+        * @see \wcf\system\io\File::__call($function, $arguments)
+        */
+       public function __call($function, $arguments) {
+               if ($this->isFlushed) {
+                       throw new SystemException('AtomicWriter for '.$this->targetFilename.' was already flushed.');
+               }
+               
+               switch ($function) {
+                       case 'write':
+                       case 'puts':
+                       case 'seek':
+                       case 'tell':
+                       case 'rewind':
+                       case 'truncate':
+                               // these are fine
+                       break;
+                       default:
+                               throw new SystemException("AtomicWriter does not allow '".$function."'");
+               }
+               
+               return parent::__call($function, $arguments);
+       }
+}