Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Filesystem] Introduce filesystem component #144

Merged
merged 1 commit into from
Mar 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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>
azjezz marked this conversation as resolved.
Show resolved Hide resolved
</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
azjezz marked this conversation as resolved.
Show resolved Hide resolved
{
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
}