Skip to content

Commit

Permalink
[Filesystem] Introduce filesystem component
Browse files Browse the repository at this point in the history
  • Loading branch information
azjezz committed Mar 8, 2021
1 parent f32efe4 commit aee410f
Show file tree
Hide file tree
Showing 52 changed files with 2,175 additions and 1 deletion.
14 changes: 14 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,26 @@
<directory>src</directory>
</include>
<exclude>
<!-- Internal -->
<file>src/preload.php</file>
<file>src/bootstrap.php</file>
<directory>src/Psl/Internal</directory>

<!-- Base Exceptions -->
<directory>src/Psl/Exception</directory>

<!-- Constants -->
<file>src/Psl/Str/constants.php</file>
<file>src/Psl/Math/constants.php</file>
<file>src/Psl/Filesystem/constants.php</file>

<!-- Things that are not easily tested -->
<file>src/Psl/Filesystem/get_group.php</file>
<file>src/Psl/Filesystem/change_group.php</file>
<file>src/Psl/Filesystem/Internal/change_group.php</file>
<file>src/Psl/Filesystem/get_owner.php</file>
<file>src/Psl/Filesystem/change_owner.php</file>
<file>src/Psl/Filesystem/Internal/change_owner.php</file>
</exclude>
<report>
<clover outputFile="tests/logs/clover.xml"/>
Expand Down
11 changes: 11 additions & 0 deletions src/Psl/Filesystem/Exception/ExceptionInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Psl\Filesystem\Exception;

use Psl\Exception;

interface ExceptionInterface extends Exception\ExceptionInterface
{
}
11 changes: 11 additions & 0 deletions src/Psl/Filesystem/Exception/RuntimeException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Psl\Filesystem\Exception;

use Psl\Exception;

class RuntimeException extends Exception\RuntimeException implements ExceptionInterface
{
}
67 changes: 67 additions & 0 deletions src/Psl/Filesystem/Internal/write_file.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace Psl\Filesystem\Internal;

use Psl;
use Psl\Filesystem;
use Psl\Filesystem\Exception;
use Psl\Internal;
use Psl\Str;

use function file_put_contents;

use const FILE_APPEND;
use const LOCK_EX;

/**
* Write $content to $file.
*
* @param bool $append If true, and $file exists, append $content to $file instead of overwriting it.
*
* @throws Psl\Exception\InvariantViolationException If the file specified by
* $file does not exist, or is not writeable.
* @throws Exception\RuntimeException In case of an error.
*/
function write_file(string $file, string $content, bool $append): void
{
if (Filesystem\exists($file)) {
Psl\invariant(Filesystem\is_file($file), '$file is not a file.');
Psl\invariant(Filesystem\is_writable($file), '$file is not writeable.');
} else {
Filesystem\create_file($file);
}

if ($append) {
// Mutually exclusive with LOCK_EX since appends are atomic
// and thus there is no reason to lock.
$flags = FILE_APPEND;
} else {
$flags = LOCK_EX;
}

[$written, $error] = Internal\box(
static fn() => file_put_contents($file, $content, $flags)
);

// @codeCoverageIgnoreStart
if (false === $written || null !== $error) {
throw new Exception\RuntimeException(Str\format(
'Failed to write to file "%s": %s.',
$file,
$error ?? 'internal error',
));
}

$length = Str\Byte\length($content);
if ($written !== $length) {
throw new Exception\RuntimeException(Str\format(
'Failed to write the whole content to "%s" ( %g of %g bytes written ).',
$file,
$written,
$length,
));
}
// @codeCoverageIgnoreEnd
}
21 changes: 21 additions & 0 deletions src/Psl/Filesystem/append_file.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Psl\Filesystem;

use Psl;

/**
* Append $content to $file.
*
* If $file does not exist, it will be created.
*
* @throws Psl\Exception\InvariantViolationException If the file specified by
* $file is a directory, or is not writeable.
* @throws Exception\RuntimeException In case of an error.
*/
function append_file(string $file, string $content): void
{
Internal\write_file($file, $content, true);
}
18 changes: 18 additions & 0 deletions src/Psl/Filesystem/canonicalize.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Psl\Filesystem;

use function realpath;

/**
* Returns canonicalized absolute pathname.
*
* @return string|null The canonicalized absolute pathname on success.
* The resulting path will have no symbolic link, '/./' or '/../' components.
*/
function canonicalize(string $path): ?string
{
return realpath($path) ?: null;
}
38 changes: 38 additions & 0 deletions src/Psl/Filesystem/change_group.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Psl\Filesystem;

use Psl;
use Psl\Internal;
use Psl\Str;

use function chgrp;
use function lchgrp;

/**
* Change the group ownership of $filename.
*
* @throws Exception\RuntimeException If unable to change the group ownership for $filename.
* @throws Psl\Exception\InvariantViolationException If $filename does not exist.
*/
function change_group(string $filename, int $group): void
{
Psl\invariant(exists($filename), '$filename does not exist.');

if (is_symbolic_link($filename)) {
$fun = static fn(): bool => lchgrp($filename, $group);
} else {
$fun = static fn(): bool => chgrp($filename, $group);
}

[$success, $error] = Internal\box($fun);
if (!$success) {
throw new Exception\RuntimeException(Str\format(
'Failed to change the group for file "%s": %s',
$filename,
$error ?? 'internal error.',
));
}
}
37 changes: 37 additions & 0 deletions src/Psl/Filesystem/change_owner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Psl\Filesystem;

use Psl;
use Psl\Internal;
use Psl\Str;

use function chown;
use function lchown;

/**
* Change the owner of $filename.
*
* @throws Exception\RuntimeException If unable to change the ownership for $filename.
* @throws Psl\Exception\InvariantViolationException If $filename does not exist.
*/
function change_owner(string $filename, int $user): void
{
Psl\invariant(exists($filename), '$filename does not exist.');
if (is_symbolic_link($filename)) {
$fun = static fn(): bool => lchown($filename, $user);
} else {
$fun = static fn(): bool => chown($filename, $user);
}

[$success, $error] = Internal\box($fun);
if (!$success) {
throw new Exception\RuntimeException(Str\format(
'Failed to change owner for file "%s": %s',
$filename,
$error ?? 'internal error.',
));
}
}
33 changes: 33 additions & 0 deletions src/Psl/Filesystem/change_permissions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Psl\Filesystem;

use Psl;
use Psl\Internal;
use Psl\Str;

use function chmod;

/**
* Changes mode permission of $filename.
*
* @throws Exception\RuntimeException If unable to change the mode for the given $filename.
* @throws Psl\Exception\InvariantViolationException If $filename does not exists.
*/
function change_permissions(string $filename, int $permissions): void
{
Psl\invariant(exists($filename), '$filename does not exist.');

[$success, $error] = Internal\box(static fn(): bool => chmod($filename, $permissions));
// @codeCoverageIgnoreStart
if (!$success) {
throw new Exception\RuntimeException(Str\format(
'Failed to change permissions for file "%s": %s',
$filename,
$error ?? 'internal error.',
));
}
// @codeCoverageIgnoreEnd
}
12 changes: 12 additions & 0 deletions src/Psl/Filesystem/constants.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Psl\Filesystem;

use const DIRECTORY_SEPARATOR;

/**
* @var string
*/
const SEPARATOR = DIRECTORY_SEPARATOR;
72 changes: 72 additions & 0 deletions src/Psl/Filesystem/copy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace Psl\Filesystem;

use Psl;
use Psl\Internal;
use Psl\Str;

use function fclose;
use function fopen;
use function stream_copy_to_stream;

/**
* Change the group ownership of $filename.
*
* @throws Exception\RuntimeException If unable to change the group ownership for $filename.
* @throws Psl\Exception\InvariantViolationException If $source does not exist or is not readable.
*/
function copy(string $source, string $destination, bool $overwrite = false): void
{
Psl\invariant(is_file($source) && is_readable($source), '$source does not exist or is not readable.');

if (!$overwrite && is_file($destination)) {
return;
}

/**
* @psalm-suppress InvalidArgument - callable is not pure..
*/
$source_stream = Internal\suppress(static fn() => fopen($source, 'rb'));
// @codeCoverageIgnoreStart
if (false === $source_stream) {
throw new Exception\RuntimeException('Failed to open $source file for reading');
}
// @codeCoverageIgnoreEnd

/**
* @psalm-suppress InvalidArgument - callable is not pure..
*/
$destination_stream = Internal\suppress(static fn() => fopen($destination, 'wb'));
// @codeCoverageIgnoreStart
if (false === $destination_stream) {
throw new Exception\RuntimeException('Failed to open $destination file for writing.');
}
// @codeCoverageIgnoreEnd

$copied_bytes = stream_copy_to_stream($source_stream, $destination_stream);
fclose($source_stream);
fclose($destination_stream);

$total_bytes = file_size($source);

// preserve executable permission bits
change_permissions(
$destination,
get_permissions($destination) | (get_permissions($source) & 0111)
);

// @codeCoverageIgnoreStart
if ($copied_bytes !== $total_bytes) {
throw new Exception\RuntimeException(Str\format(
'Failed to copy the whole content of "%s" to "%s" ( %g of %g bytes copied ).',
$source,
$destination,
$copied_bytes,
$total_bytes
));
}
// @codeCoverageIgnoreEnd
}
32 changes: 32 additions & 0 deletions src/Psl/Filesystem/create_directory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Psl\Filesystem;

use Psl\Internal;
use Psl\Str;

use function mkdir;

/**
* Create the directory specified by $directory.
*
* @throws Exception\RuntimeException If unable to create the directory.
*/
function create_directory(string $directory, int $permissions = 0777): void
{
[$result, $error_message] = Internal\box(
static fn() => mkdir($directory, $permissions, true)
);

// @codeCoverageIgnoreStart
if (false === $result && !is_directory($directory)) {
throw new Exception\RuntimeException(Str\format(
'Failed to create directory "%s": %s.',
$directory,
$error_message ?? 'internal error'
));
}
// @codeCoverageIgnoreEnd
}

0 comments on commit aee410f

Please sign in to comment.