Skip to content

Commit 989e3db

Browse files
authored
feat: Introduce a FileSystem interface (#26)
1 parent e30b77e commit 989e3db

File tree

4 files changed

+402
-86
lines changed

4 files changed

+402
-86
lines changed

src/FS.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public static function getInstance(): FileSystem
7070
{
7171
/** @psalm-suppress RedundantPropertyInitializationCheck */
7272
if (!isset(self::$filesystem)) {
73-
self::$filesystem = new FileSystem();
73+
self::$filesystem = new NativeFileSystem();
7474
}
7575

7676
return self::$filesystem;

src/FileSystem.php

Lines changed: 7 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,8 @@
4747
namespace Fidry\FileSystem;
4848

4949
use Symfony\Component\Filesystem\Exception\IOException;
50-
use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem;
51-
use Symfony\Component\Filesystem\Path;
52-
use Webmozart\Assert\Assert;
53-
use function error_get_last;
54-
use function file_get_contents;
55-
use function random_int;
56-
use function realpath;
57-
use function sprintf;
58-
use function str_replace;
59-
use function sys_get_temp_dir;
60-
use const DIRECTORY_SEPARATOR;
6150

62-
class FileSystem extends SymfonyFilesystem
51+
interface FileSystem extends SymfonyFileSystem
6352
{
6453
/**
6554
* Returns whether a path is relative.
@@ -69,20 +58,11 @@ class FileSystem extends SymfonyFilesystem
6958
* @return bool returns true if the path is relative or empty, false if
7059
* it is absolute
7160
*/
72-
public function isRelativePath(string $path): bool
73-
{
74-
return !$this->isAbsolutePath($path);
75-
}
61+
public function isRelativePath(string $path): bool;
7662

77-
public function escapePath(string $path): string
78-
{
79-
return str_replace('/', DIRECTORY_SEPARATOR, $path);
80-
}
63+
public function escapePath(string $path): string;
8164

82-
public function dumpFile(string $filename, $content = ''): void
83-
{
84-
parent::dumpFile($filename, $content);
85-
}
65+
public function dumpFile(string $filename, $content = ''): void;
8666

8767
/**
8868
* Gets the contents of a file.
@@ -93,26 +73,7 @@ public function dumpFile(string $filename, $content = ''): void
9373
*
9474
* @return string File contents
9575
*/
96-
public function getFileContents(string $file): string
97-
{
98-
Assert::file($file);
99-
Assert::readable($file);
100-
101-
if (false === ($contents = @file_get_contents($file))) {
102-
throw new IOException(
103-
sprintf(
104-
'Failed to read file "%s": %s.',
105-
$file,
106-
error_get_last()['message'],
107-
),
108-
0,
109-
null,
110-
$file,
111-
);
112-
}
113-
114-
return $contents;
115-
}
76+
public function getFileContents(string $file): string;
11677

11778
/**
11879
* Creates a temporary directory.
@@ -122,51 +83,12 @@ public function getFileContents(string $file): string
12283
*
12384
* @return string the path to the created directory
12485
*/
125-
public function makeTmpDir(string $namespace, string $className): string
126-
{
127-
$shortClass = false !== ($pos = mb_strrpos($className, '\\'))
128-
? mb_substr($className, $pos + 1)
129-
: $className;
130-
131-
$basePath = $this->getNamespacedTmpDir($namespace).'/'.$shortClass;
132-
133-
$result = false;
134-
$attempts = 0;
135-
136-
do {
137-
$tmpDir = $this->escapePath($basePath.random_int(10000, 99999));
138-
139-
if ($this->exists($tmpDir)) {
140-
++$attempts;
141-
142-
continue;
143-
}
144-
145-
try {
146-
$this->mkdir($tmpDir, 0o777);
147-
148-
$result = true;
149-
} catch (IOException) {
150-
++$attempts;
151-
}
152-
} while (false === $result && $attempts <= 10);
153-
154-
return $tmpDir;
155-
}
86+
public function makeTmpDir(string $namespace, string $className): string;
15687

15788
/**
15889
* Gets a namespaced temporary directory.
15990
*
16091
* @param string $namespace the directory path in the system's temporary directory
16192
*/
162-
public function getNamespacedTmpDir(string $namespace): string
163-
{
164-
// Usage of realpath() is important if the temporary directory is a
165-
// symlink to another directory (e.g. /var => /private/var on some Macs)
166-
// We want to know the real path to avoid comparison failures with
167-
// code that uses real paths only
168-
$systemTempDir = str_replace('\\', '/', realpath(sys_get_temp_dir()));
169-
170-
return $systemTempDir.'/'.$namespace;
171-
}
93+
public function getNamespacedTmpDir(string $namespace): string;
17294
}

src/NativeFileSystem.php

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<?php
2+
3+
/*
4+
* This code is licensed under the BSD 3-Clause License.
5+
*
6+
* Copyright (c) 2022, Théo FIDRY <[email protected]>
7+
* All rights reserved.
8+
*
9+
* Redistribution and use in source and binary forms, with or without
10+
* modification, are permitted provided that the following conditions are met:
11+
*
12+
* * Redistributions of source code must retain the above copyright notice, this
13+
* list of conditions and the following disclaimer.
14+
*
15+
* * Redistributions in binary form must reproduce the above copyright notice,
16+
* this list of conditions and the following disclaimer in the documentation
17+
* and/or other materials provided with the distribution.
18+
*
19+
* * Neither the name of the copyright holder nor the names of its
20+
* contributors may be used to endorse or promote products derived from
21+
* this software without specific prior written permission.
22+
*
23+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
27+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33+
*/
34+
35+
declare(strict_types=1);
36+
37+
/*
38+
* This file is part of the box project.
39+
*
40+
* (c) Kevin Herrera <[email protected]>
41+
* Théo Fidry <[email protected]>
42+
*
43+
* This source file is subject to the MIT license that is bundled
44+
* with this source code in the file LICENSE.
45+
*/
46+
47+
namespace Fidry\FileSystem;
48+
49+
use Symfony\Component\Filesystem\Exception\IOException;
50+
use Symfony\Component\Filesystem\Filesystem as NativeSymfonyFilesystem;
51+
use Webmozart\Assert\Assert;
52+
use function error_get_last;
53+
use function file_get_contents;
54+
use function random_int;
55+
use function realpath;
56+
use function sprintf;
57+
use function str_replace;
58+
use function sys_get_temp_dir;
59+
use const DIRECTORY_SEPARATOR;
60+
61+
class NativeFileSystem extends NativeSymfonyFilesystem implements FileSystem
62+
{
63+
public function isRelativePath(string $path): bool
64+
{
65+
return !$this->isAbsolutePath($path);
66+
}
67+
68+
public function escapePath(string $path): string
69+
{
70+
return str_replace('/', DIRECTORY_SEPARATOR, $path);
71+
}
72+
73+
public function dumpFile(string $filename, $content = ''): void
74+
{
75+
parent::dumpFile($filename, $content);
76+
}
77+
78+
public function getFileContents(string $file): string
79+
{
80+
Assert::file($file);
81+
Assert::readable($file);
82+
83+
if (false === ($contents = @file_get_contents($file))) {
84+
throw new IOException(
85+
sprintf(
86+
'Failed to read file "%s": %s.',
87+
$file,
88+
error_get_last()['message'],
89+
),
90+
0,
91+
null,
92+
$file,
93+
);
94+
}
95+
96+
return $contents;
97+
}
98+
99+
public function makeTmpDir(string $namespace, string $className): string
100+
{
101+
$shortClass = false !== ($pos = mb_strrpos($className, '\\'))
102+
? mb_substr($className, $pos + 1)
103+
: $className;
104+
105+
$basePath = $this->getNamespacedTmpDir($namespace).'/'.$shortClass;
106+
107+
$result = false;
108+
$attempts = 0;
109+
110+
do {
111+
$tmpDir = $this->escapePath($basePath.random_int(10000, 99999));
112+
113+
if ($this->exists($tmpDir)) {
114+
++$attempts;
115+
116+
continue;
117+
}
118+
119+
try {
120+
$this->mkdir($tmpDir, 0o777);
121+
122+
$result = true;
123+
} catch (IOException) {
124+
++$attempts;
125+
}
126+
} while (false === $result && $attempts <= 10);
127+
128+
return $tmpDir;
129+
}
130+
131+
public function getNamespacedTmpDir(string $namespace): string
132+
{
133+
// Usage of realpath() is important if the temporary directory is a
134+
// symlink to another directory (e.g. /var => /private/var on some Macs)
135+
// We want to know the real path to avoid comparison failures with
136+
// code that uses real paths only
137+
$systemTempDir = str_replace('\\', '/', realpath(sys_get_temp_dir()));
138+
139+
return $systemTempDir.'/'.$namespace;
140+
}
141+
}

0 commit comments

Comments
 (0)