diff options
-rw-r--r-- | application/FileUtils.php | 75 | ||||
-rw-r--r-- | application/LinkDB.php | 31 | ||||
-rw-r--r-- | application/exceptions/IOException.php | 22 | ||||
-rw-r--r-- | tests/FileUtilsTest.php | 108 | ||||
-rw-r--r-- | tests/LinkDBTest.php | 2 |
5 files changed, 198 insertions, 40 deletions
diff --git a/application/FileUtils.php b/application/FileUtils.php index 6cac9825..b8ad8970 100644 --- a/application/FileUtils.php +++ b/application/FileUtils.php | |||
@@ -1,21 +1,76 @@ | |||
1 | <?php | 1 | <?php |
2 | |||
3 | require_once 'exceptions/IOException.php'; | ||
4 | |||
2 | /** | 5 | /** |
3 | * Exception class thrown when a filesystem access failure happens | 6 | * Class FileUtils |
7 | * | ||
8 | * Utility class for file manipulation. | ||
4 | */ | 9 | */ |
5 | class IOException extends Exception | 10 | class FileUtils |
6 | { | 11 | { |
7 | private $path; | 12 | /** |
13 | * @var string | ||
14 | */ | ||
15 | protected static $phpPrefix = '<?php /* '; | ||
16 | |||
17 | /** | ||
18 | * @var string | ||
19 | */ | ||
20 | protected static $phpSuffix = ' */ ?>'; | ||
8 | 21 | ||
9 | /** | 22 | /** |
10 | * Construct a new IOException | 23 | * Write data into a file (Shaarli database format). |
24 | * The data is stored in a PHP file, as a comment, in compressed base64 format. | ||
25 | * | ||
26 | * The file will be created if it doesn't exist. | ||
27 | * | ||
28 | * @param string $file File path. | ||
29 | * @param string $content Content to write. | ||
30 | * | ||
31 | * @return int|bool Number of bytes written or false if it fails. | ||
11 | * | 32 | * |
12 | * @param string $path path to the resource that cannot be accessed | 33 | * @throws IOException The destination file can't be written. |
13 | * @param string $message Custom exception message. | ||
14 | */ | 34 | */ |
15 | public function __construct($path, $message = '') | 35 | public static function writeFlatDB($file, $content) |
16 | { | 36 | { |
17 | $this->path = $path; | 37 | if (is_file($file) && !is_writeable($file)) { |
18 | $this->message = empty($message) ? 'Error accessing' : $message; | 38 | // The datastore exists but is not writeable |
19 | $this->message .= PHP_EOL . $this->path; | 39 | throw new IOException($file); |
40 | } else if (!is_file($file) && !is_writeable(dirname($file))) { | ||
41 | // The datastore does not exist and its parent directory is not writeable | ||
42 | throw new IOException(dirname($file)); | ||
43 | } | ||
44 | |||
45 | return file_put_contents( | ||
46 | $file, | ||
47 | self::$phpPrefix.base64_encode(gzdeflate(serialize($content))).self::$phpSuffix | ||
48 | ); | ||
49 | } | ||
50 | |||
51 | /** | ||
52 | * Read data from a file containing Shaarli database format content. | ||
53 | * If the file isn't readable or doesn't exists, default data will be returned. | ||
54 | * | ||
55 | * @param string $file File path. | ||
56 | * @param mixed $default The default value to return if the file isn't readable. | ||
57 | * | ||
58 | * @return mixed The content unserialized, or default if the file isn't readable, or false if it fails. | ||
59 | */ | ||
60 | public static function readFlatDB($file, $default = null) | ||
61 | { | ||
62 | // Note that gzinflate is faster than gzuncompress. | ||
63 | // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 | ||
64 | if (is_readable($file)) { | ||
65 | return unserialize( | ||
66 | gzinflate( | ||
67 | base64_decode( | ||
68 | substr(file_get_contents($file), strlen(self::$phpPrefix), -strlen(self::$phpSuffix)) | ||
69 | ) | ||
70 | ) | ||
71 | ); | ||
72 | } | ||
73 | |||
74 | return $default; | ||
20 | } | 75 | } |
21 | } | 76 | } |
diff --git a/application/LinkDB.php b/application/LinkDB.php index 4cee2af9..2fb15040 100644 --- a/application/LinkDB.php +++ b/application/LinkDB.php | |||
@@ -50,12 +50,6 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
50 | // Link date storage format | 50 | // Link date storage format |
51 | const LINK_DATE_FORMAT = 'Ymd_His'; | 51 | const LINK_DATE_FORMAT = 'Ymd_His'; |
52 | 52 | ||
53 | // Datastore PHP prefix | ||
54 | protected static $phpPrefix = '<?php /* '; | ||
55 | |||
56 | // Datastore PHP suffix | ||
57 | protected static $phpSuffix = ' */ ?>'; | ||
58 | |||
59 | // List of links (associative array) | 53 | // List of links (associative array) |
60 | // - key: link date (e.g. "20110823_124546"), | 54 | // - key: link date (e.g. "20110823_124546"), |
61 | // - value: associative array (keys: title, description...) | 55 | // - value: associative array (keys: title, description...) |
@@ -295,16 +289,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
295 | return; | 289 | return; |
296 | } | 290 | } |
297 | 291 | ||
298 | // Read data | 292 | $this->links = FileUtils::readFlatDB($this->datastore, []); |
299 | // Note that gzinflate is faster than gzuncompress. | ||
300 | // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 | ||
301 | $this->links = array(); | ||
302 | |||
303 | if (file_exists($this->datastore)) { | ||
304 | $this->links = unserialize(gzinflate(base64_decode( | ||
305 | substr(file_get_contents($this->datastore), | ||
306 | strlen(self::$phpPrefix), -strlen(self::$phpSuffix))))); | ||
307 | } | ||
308 | 293 | ||
309 | $toremove = array(); | 294 | $toremove = array(); |
310 | foreach ($this->links as $key => &$link) { | 295 | foreach ($this->links as $key => &$link) { |
@@ -361,19 +346,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
361 | */ | 346 | */ |
362 | private function write() | 347 | private function write() |
363 | { | 348 | { |
364 | if (is_file($this->datastore) && !is_writeable($this->datastore)) { | 349 | FileUtils::writeFlatDB($this->datastore, $this->links); |
365 | // The datastore exists but is not writeable | ||
366 | throw new IOException($this->datastore); | ||
367 | } else if (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) { | ||
368 | // The datastore does not exist and its parent directory is not writeable | ||
369 | throw new IOException(dirname($this->datastore)); | ||
370 | } | ||
371 | |||
372 | file_put_contents( | ||
373 | $this->datastore, | ||
374 | self::$phpPrefix.base64_encode(gzdeflate(serialize($this->links))).self::$phpSuffix | ||
375 | ); | ||
376 | |||
377 | } | 350 | } |
378 | 351 | ||
379 | /** | 352 | /** |
diff --git a/application/exceptions/IOException.php b/application/exceptions/IOException.php new file mode 100644 index 00000000..b563b23d --- /dev/null +++ b/application/exceptions/IOException.php | |||
@@ -0,0 +1,22 @@ | |||
1 | <?php | ||
2 | |||
3 | /** | ||
4 | * Exception class thrown when a filesystem access failure happens | ||
5 | */ | ||
6 | class IOException extends Exception | ||
7 | { | ||
8 | private $path; | ||
9 | |||
10 | /** | ||
11 | * Construct a new IOException | ||
12 | * | ||
13 | * @param string $path path to the resource that cannot be accessed | ||
14 | * @param string $message Custom exception message. | ||
15 | */ | ||
16 | public function __construct($path, $message = '') | ||
17 | { | ||
18 | $this->path = $path; | ||
19 | $this->message = empty($message) ? 'Error accessing' : $message; | ||
20 | $this->message .= ' "' . $this->path .'"'; | ||
21 | } | ||
22 | } | ||
diff --git a/tests/FileUtilsTest.php b/tests/FileUtilsTest.php new file mode 100644 index 00000000..d764e495 --- /dev/null +++ b/tests/FileUtilsTest.php | |||
@@ -0,0 +1,108 @@ | |||
1 | <?php | ||
2 | |||
3 | require_once 'application/FileUtils.php'; | ||
4 | |||
5 | /** | ||
6 | * Class FileUtilsTest | ||
7 | * | ||
8 | * Test file utility class. | ||
9 | */ | ||
10 | class FileUtilsTest extends PHPUnit_Framework_TestCase | ||
11 | { | ||
12 | /** | ||
13 | * @var string Test file path. | ||
14 | */ | ||
15 | protected static $file = 'sandbox/flat.db'; | ||
16 | |||
17 | /** | ||
18 | * Delete test file after every test. | ||
19 | */ | ||
20 | public function tearDown() | ||
21 | { | ||
22 | @unlink(self::$file); | ||
23 | } | ||
24 | |||
25 | /** | ||
26 | * Test writeDB, then readDB with different data. | ||
27 | */ | ||
28 | public function testSimpleWriteRead() | ||
29 | { | ||
30 | $data = ['blue', 'red']; | ||
31 | $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0); | ||
32 | $this->assertTrue(startsWith(file_get_contents(self::$file), '<?php /*')); | ||
33 | $this->assertEquals($data, FileUtils::readFlatDB(self::$file)); | ||
34 | |||
35 | $data = 0; | ||
36 | $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0); | ||
37 | $this->assertEquals($data, FileUtils::readFlatDB(self::$file)); | ||
38 | |||
39 | $data = null; | ||
40 | $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0); | ||
41 | $this->assertEquals($data, FileUtils::readFlatDB(self::$file)); | ||
42 | |||
43 | $data = false; | ||
44 | $this->assertTrue(FileUtils::writeFlatDB(self::$file, $data) > 0); | ||
45 | $this->assertEquals($data, FileUtils::readFlatDB(self::$file)); | ||
46 | } | ||
47 | |||
48 | /** | ||
49 | * File not writable: raise an exception. | ||
50 | * | ||
51 | * @expectedException IOException | ||
52 | * @expectedExceptionMessage Error accessing "sandbox/flat.db" | ||
53 | */ | ||
54 | public function testWriteWithoutPermission() | ||
55 | { | ||
56 | touch(self::$file); | ||
57 | chmod(self::$file, 0440); | ||
58 | FileUtils::writeFlatDB(self::$file, null); | ||
59 | } | ||
60 | |||
61 | /** | ||
62 | * Folder non existent: raise an exception. | ||
63 | * | ||
64 | * @expectedException IOException | ||
65 | * @expectedExceptionMessage Error accessing "nopefolder" | ||
66 | */ | ||
67 | public function testWriteFolderDoesNotExist() | ||
68 | { | ||
69 | FileUtils::writeFlatDB('nopefolder/file', null); | ||
70 | } | ||
71 | |||
72 | /** | ||
73 | * Folder non writable: raise an exception. | ||
74 | * | ||
75 | * @expectedException IOException | ||
76 | * @expectedExceptionMessage Error accessing "sandbox" | ||
77 | */ | ||
78 | public function testWriteFolderPermission() | ||
79 | { | ||
80 | chmod(dirname(self::$file), 0555); | ||
81 | try { | ||
82 | FileUtils::writeFlatDB(self::$file, null); | ||
83 | } catch (Exception $e) { | ||
84 | chmod(dirname(self::$file), 0755); | ||
85 | throw $e; | ||
86 | } | ||
87 | } | ||
88 | |||
89 | /** | ||
90 | * Read non existent file, use default parameter. | ||
91 | */ | ||
92 | public function testReadNotExistentFile() | ||
93 | { | ||
94 | $this->assertEquals(null, FileUtils::readFlatDB(self::$file)); | ||
95 | $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test'])); | ||
96 | } | ||
97 | |||
98 | /** | ||
99 | * Read non readable file, use default parameter. | ||
100 | */ | ||
101 | public function testReadNotReadable() | ||
102 | { | ||
103 | touch(self::$file); | ||
104 | chmod(self::$file, 0220); | ||
105 | $this->assertEquals(null, FileUtils::readFlatDB(self::$file)); | ||
106 | $this->assertEquals(['test'], FileUtils::readFlatDB(self::$file, ['test'])); | ||
107 | } | ||
108 | } | ||
diff --git a/tests/LinkDBTest.php b/tests/LinkDBTest.php index 1f62a34a..7bf98f92 100644 --- a/tests/LinkDBTest.php +++ b/tests/LinkDBTest.php | |||
@@ -101,7 +101,7 @@ class LinkDBTest extends PHPUnit_Framework_TestCase | |||
101 | * Attempt to instantiate a LinkDB whereas the datastore is not writable | 101 | * Attempt to instantiate a LinkDB whereas the datastore is not writable |
102 | * | 102 | * |
103 | * @expectedException IOException | 103 | * @expectedException IOException |
104 | * @expectedExceptionMessageRegExp /Error accessing\nnull/ | 104 | * @expectedExceptionMessageRegExp /Error accessing "null"/ |
105 | */ | 105 | */ |
106 | public function testConstructDatastoreNotWriteable() | 106 | public function testConstructDatastoreNotWriteable() |
107 | { | 107 | { |