mirror of
https://github.com/DirectoryLister/DirectoryLister.git
synced 2025-08-29 08:40:42 +02:00
Merge 'dev/3.0.0' branch into master
This commit is contained in:
9
.docker/Dockerfile.apache
Normal file
9
.docker/Dockerfile.apache
Normal file
@@ -0,0 +1,9 @@
|
||||
# Build apache image
|
||||
FROM php:7.4-apache
|
||||
LABEL maintainer="Chris Kankiewicz <Chris@ChrisKankiewicz.com>"
|
||||
|
||||
COPY ./php/config/php.ini /usr/local/etc/php/php.ini
|
||||
COPY ./apache/config/000-default.conf /etc/apache2/sites-available/000-default.conf
|
||||
|
||||
RUN a2enmod rewrite
|
||||
RUN pecl install xdebug && docker-php-ext-enable xdebug
|
7
.docker/Dockerfile.nginx
Normal file
7
.docker/Dockerfile.nginx
Normal file
@@ -0,0 +1,7 @@
|
||||
# Build php-fpm image
|
||||
FROM php:7.4-fpm
|
||||
LABEL maintainer="Chris Kankiewicz <Chris@ChrisKankiewicz.com>"
|
||||
|
||||
COPY ./php/config/php.ini /usr/local/etc/php/php.ini
|
||||
|
||||
RUN pecl install xdebug && docker-php-ext-enable xdebug
|
9
.docker/apache/config/000-default.conf
Normal file
9
.docker/apache/config/000-default.conf
Normal file
@@ -0,0 +1,9 @@
|
||||
<VirtualHost *:80>
|
||||
DocumentRoot /var/www/html
|
||||
ServerAdmin Chris@ChrisKankiewicz.com
|
||||
|
||||
LogLevel debug
|
||||
|
||||
ErrorLog /dev/stderr
|
||||
CustomLog /dev/stderr combined
|
||||
</VirtualHost>
|
30
.docker/nginx/config/default.conf
Normal file
30
.docker/nginx/config/default.conf
Normal file
@@ -0,0 +1,30 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
root /var/www/html;
|
||||
index index.php;
|
||||
|
||||
access_log /dev/stderr;
|
||||
error_log /dev/stderr;
|
||||
|
||||
client_max_body_size 0;
|
||||
|
||||
location = /favicon.ico { access_log off; log_not_found off; }
|
||||
location = /robots.txt { access_log off; log_not_found off; }
|
||||
|
||||
location / {
|
||||
try_files $uri /index.php$is_args$args;
|
||||
}
|
||||
|
||||
location ~ \.php {
|
||||
fastcgi_split_path_info ^(.+\.php)(.*)$;
|
||||
|
||||
fastcgi_param HTTP_PROXY "";
|
||||
|
||||
fastcgi_pass php-fpm:9000;
|
||||
fastcgi_index index.php;
|
||||
|
||||
include fastcgi_params;
|
||||
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
}
|
||||
}
|
4
.docker/php/config/php.ini
Normal file
4
.docker/php/config/php.ini
Normal file
@@ -0,0 +1,4 @@
|
||||
[PHP]
|
||||
error_reporting = E_ALL
|
||||
log_errors = On
|
||||
memory_limit = -1
|
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
.git
|
||||
node_modules
|
||||
vendor
|
24
.editorconfig
Normal file
24
.editorconfig
Normal file
@@ -0,0 +1,24 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.json]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
[Makefile]
|
||||
indent_size = 4
|
||||
indent_style = tab
|
13
.env.example
Normal file
13
.env.example
Normal file
@@ -0,0 +1,13 @@
|
||||
DARK_MODE=false
|
||||
|
||||
SORT_ORDER=type
|
||||
REVERSE_SORT=false
|
||||
|
||||
HIDE_APP_FILES=true
|
||||
HIDE_VCS_FILES=true
|
||||
|
||||
DISPLAY_READMES=true
|
||||
|
||||
DATE_FORMAT="Y-m-d H:i:s"
|
||||
|
||||
MAX_HASH_SIZE=1000000000
|
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @PHLAK
|
35
.github/CONTRIBUTING
vendored
Normal file
35
.github/CONTRIBUTING
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# Contributing
|
||||
|
||||
Contributions are **welcome** via Pull Requests on [GitHub](https://github.com/DirectoryLister/DirectoryLister).
|
||||
|
||||
**Before contributing** we encourage you to discuss the change on the
|
||||
[Spectrum Community](https://spectrum.chat/directory-lister) to verify
|
||||
fit with the overall direction and goals of Directory Lister.
|
||||
|
||||
## Pull Requests Requirements
|
||||
|
||||
- **[PSR-2 Coding Standard.](https://www.php-fig.org/psr/psr-2/)** The easiest
|
||||
way to apply the conventions is to install and run
|
||||
[PHP CS Fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer).
|
||||
|
||||
- **Test all the tings!** Your patch won't be accepted if it doesn't have tests.
|
||||
|
||||
- **Document changes in behaviour.** Make sure relevant documentation is kept
|
||||
up to date.
|
||||
|
||||
- **One feature per pull request.** If you want to do change more than one
|
||||
thing, send multiple pull requests.
|
||||
|
||||
## Checking your work
|
||||
|
||||
### Static Analysis
|
||||
|
||||
$ vendor/bin/psalm
|
||||
|
||||
### Run Tests
|
||||
|
||||
$ vendor/bin/phpunit
|
||||
|
||||
---
|
||||
|
||||
*Thank you and happy coding!*
|
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
github: PHLAK
|
||||
patreon: PHLAK
|
||||
custom: https://paypal.me/ChrisKankiewicz
|
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us fix something that is broken
|
||||
title: ''
|
||||
labels: Bug
|
||||
assignees: PHLAK
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
A description of the observed behavior.
|
||||
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
1. Do something...
|
||||
2. Followed by something else...
|
||||
3. Then do another thing...
|
||||
4. See error
|
||||
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
A description of what you expected to happen.
|
||||
|
||||
|
||||
**Additional context**
|
||||
|
||||
Add any other context about the problem here.
|
27
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
27
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: Enhancement
|
||||
assignees: PHLAK
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
|
||||
A description of what the problem is. Ex. I'm always frustrated when...
|
||||
|
||||
|
||||
**Describe the solution you'd like**
|
||||
|
||||
A description of what you want to happen.
|
||||
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
|
||||
A description of any alternative solutions or features you've considered.
|
||||
|
||||
|
||||
**Additional context**
|
||||
|
||||
Add any other context or screenshots about the feature request here.
|
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/app/dist/
|
||||
/node_modules/
|
||||
/vendor/
|
||||
.env
|
||||
.php_cs.cache
|
||||
.phpunit.result.cache
|
||||
/mix-manifest.json
|
12
.htaccess
Normal file
12
.htaccess
Normal file
@@ -0,0 +1,12 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
|
||||
# Redirect Trailing Slashes If Not A Folder...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Handle Front Controller...
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
</IfModule>
|
33
.php_cs.dist
Normal file
33
.php_cs.dist
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
$finder = PhpCsFixer\Finder::create()->in([
|
||||
__DIR__ . DIRECTORY_SEPARATOR . 'app',
|
||||
__DIR__ . DIRECTORY_SEPARATOR . 'tests'
|
||||
])->notPath('cache');
|
||||
|
||||
return PhpCsFixer\Config::create()->setRules([
|
||||
'@PSR1' => true,
|
||||
'@PSR2' => true,
|
||||
'@Symfony' => true,
|
||||
'array_indentation' => true,
|
||||
'array_syntax' => [
|
||||
'syntax' => 'short'
|
||||
],
|
||||
'concat_space' => [
|
||||
'spacing' => 'one'
|
||||
],
|
||||
'linebreak_after_opening_tag' => true,
|
||||
'new_with_braces' => false,
|
||||
'no_multiline_whitespace_before_semicolons' => true,
|
||||
'no_superfluous_phpdoc_tags' => false,
|
||||
'no_useless_else' => true,
|
||||
'no_useless_return' => true,
|
||||
'not_operator_with_successor_space' => true,
|
||||
'phpdoc_to_comment' => false,
|
||||
'phpdoc_no_empty_return' => false,
|
||||
'phpdoc_order' => true,
|
||||
'semicolon_after_instruction' => true,
|
||||
'single_trait_insert_per_statement' => false,
|
||||
'trailing_comma_in_multiline_array' => false,
|
||||
'yoda_style' => null
|
||||
])->setFinder($finder);
|
20
.styleci.yml
Normal file
20
.styleci.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
preset: laravel
|
||||
|
||||
risky: false
|
||||
|
||||
enabled:
|
||||
- concat_with_spaces
|
||||
|
||||
disabled:
|
||||
- concat_without_spaces
|
||||
- post_increment
|
||||
- trailing_comma_in_multiline_array
|
||||
|
||||
finder:
|
||||
exclude:
|
||||
- "vendor"
|
||||
name:
|
||||
- "*.php"
|
||||
path:
|
||||
- "app"
|
||||
- "tests"
|
27
.travis.yml
Normal file
27
.travis.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
sudo: false
|
||||
|
||||
language: php
|
||||
|
||||
php:
|
||||
- 7.3
|
||||
- 7.4
|
||||
- nightly
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- php: nightly
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache
|
||||
- vendor
|
||||
|
||||
before_install: phpenv config-rm xdebug.ini || true
|
||||
|
||||
install: composer install
|
||||
|
||||
before_script:
|
||||
- vendor/bin/php-cs-fixer fix --diff --dry-run
|
||||
- vendor/bin/psalm
|
||||
|
||||
script: vendor/bin/phpunit --coverage-text
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Chris Kankiewicz <Chris@ChrisKankiewicz.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
33
Makefile
Normal file
33
Makefile
Normal file
@@ -0,0 +1,33 @@
|
||||
ARTIFACT_FILES=$$(paste --delimiters ' ' --serial artifacts.include)
|
||||
ARTIFACT_NAME="DirectoryLister-$$(git describe --tags --exact-match HEAD 2> /dev/null || git rev-parse --short HEAD)"
|
||||
|
||||
dev development: # Build application for development
|
||||
@composer install --no-interaction
|
||||
@npm install && npm run dev
|
||||
|
||||
prod production: # Build application for production
|
||||
@composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
|
||||
@npm install --no-save && npm run production && npm prune --production
|
||||
|
||||
update upgrade: # Update application dependencies
|
||||
@composer update && npm update && npm install
|
||||
|
||||
test: # Run coding standards/static analysis checks and tests
|
||||
@php-cs-fixer fix --diff --dry-run && psalm --show-info=false && phpunit --coverage-text
|
||||
|
||||
tunnel: # Expose the application via secure tunnel
|
||||
@ngrok http -host-header=rewrite http://directory-lister.local:80
|
||||
|
||||
clear-cache: # Clear the application cache
|
||||
@rm app/cache/* -rfv
|
||||
|
||||
tar: # Generate tarball
|
||||
@tar --verbose --create --gzip --exclude-vcs --exclude-vcs-ignores \
|
||||
--exclude app/cache/* --file artifacts/$(ARTIFACT_NAME).tar.gz \
|
||||
$(ARTIFACT_FILES)
|
||||
|
||||
zip: # Generate zip file
|
||||
@zip --exclude "app/cache/**" --exclude "*.git*" \
|
||||
--recurse-paths artifacts/$(ARTIFACT_NAME).zip $(ARTIFACT_FILES)
|
||||
|
||||
artifacts: clear-cache production tar zip # Generate release artifacts
|
64
README.md
Normal file
64
README.md
Normal file
@@ -0,0 +1,64 @@
|
||||
<p align="center">
|
||||
<img src="directory-lister.svg" alt="Directory Lister" width="66%">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/DirectoryLister/DirectoryLister/blob/master/LICENSE"><img src="https://img.shields.io/github/license/DirectoryLister/DirectoryLister?style=flat-square" alt="License"></a>
|
||||
<a href="https://travis-ci.org/DirectoryLister/DirectoryLister"><img src="https://img.shields.io/travis/DirectoryLister/DirectoryLister.svg?style=flat-square" alt="Build Status"></a>
|
||||
<a href="https://styleci.io/repos/1375774"><img src="https://styleci.io/repos/1375774/shield?branch=master" alt="StyleCI"></a>
|
||||
<a href="https://www.ChrisKankiewicz.com"><img src="https://img.shields.io/badge/created_by-Chris%20Kankiewicz-319795.svg?style=flat-square" alt="Author"></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
Directory Lister is the easiest way to expose the contents of any web-accessable
|
||||
folder for browsing and sharing. With a zero configuration, drag-and-drop
|
||||
installation you'll be up and running in less than a minute.
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- **Simple installation** allows you to be up and running in less than a minute.
|
||||
- **Light and dark themes** to suit your professional needs or personal style.
|
||||
- **Custom sort ordering** gives you control of the ordering of your files/folders.
|
||||
- **File search** helps you locate the files you need quickly and efficiently.
|
||||
- **File hashes** instill confidence when downloading files through verification.
|
||||
- **Readme rendering** allows exposing the contents of READMEs directly on the page.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
- [PHP](https://php.net) >= 7.2
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
1. [Downoad Directory Lister](https://github.com/DirectoryLister/DirectoryLister/releases)
|
||||
2. Extract the zip/tar archive
|
||||
3. Copy extracted files/folders to your web server
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
1. Copy `.env.example` to `.env`
|
||||
2. Edit the configuration values in `.env`
|
||||
|
||||
Changelog
|
||||
---------
|
||||
|
||||
A list of changes can be found on the [GitHub Releases](https://github.com/DirectoryLister/DirectoryLister/releases) page.
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
For general help and support join our [Spectrum Community](https://spectrum.chat/directory-lister).
|
||||
|
||||
Additionally join our [Slack Workspace](https://ln.phlak.net/join-slack) or
|
||||
reach out on [Twitter](https://twitter.com/DirectoryLister).
|
||||
|
||||
Please report bugs to the [GitHub Issue Tracker](https://github.com/DirectoryLister/DirectoryLister/issues).
|
||||
|
||||
Copyright
|
||||
---------
|
||||
|
||||
This project is licensed under the [MIT License](https://github.com/DirectoryLister/DirectoryLister/blob/master/LICENSE).
|
70
app/Bootstrap/AppManager.php
Normal file
70
app/Bootstrap/AppManager.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Bootstrap;
|
||||
|
||||
use App\Providers;
|
||||
use DI\Bridge\Slim\Bridge;
|
||||
use DI\Container;
|
||||
use Invoker\CallableResolver;
|
||||
use PHLAK\Config\Config;
|
||||
use Slim\App;
|
||||
use Tightenco\Collect\Support\Collection;
|
||||
|
||||
class AppManager
|
||||
{
|
||||
/** @const Array of application providers */
|
||||
protected const PROVIDERS = [
|
||||
Providers\ConfigProvider::class,
|
||||
Providers\FinderProvider::class,
|
||||
Providers\TwigProvider::class,
|
||||
];
|
||||
|
||||
/** @var Container The applicaiton container */
|
||||
protected $container;
|
||||
|
||||
/** @var Config The application config */
|
||||
protected $config;
|
||||
|
||||
/** @var CallableResolver The callable resolver */
|
||||
protected $callableResolver;
|
||||
|
||||
/**
|
||||
* Create a new Provider object.
|
||||
*
|
||||
* @param \DI\Container $container
|
||||
*/
|
||||
public function __construct(Container $container, Config $config, CallableResolver $callableResolver)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->config = $config;
|
||||
$this->callableResolver = $callableResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup and configure the application.
|
||||
*
|
||||
* @return \Slim\App
|
||||
*/
|
||||
public function __invoke(): App
|
||||
{
|
||||
$this->registerProviders();
|
||||
|
||||
return Bridge::create($this->container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register application providers.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerProviders(): void
|
||||
{
|
||||
Collection::make(self::PROVIDERS)->merge(
|
||||
$this->config->get('app.providers', [])
|
||||
)->each(function (string $provider) {
|
||||
$this->container->call(
|
||||
$this->callableResolver->resolve($provider)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
131
app/Controllers/DirectoryController.php
Normal file
131
app/Controllers/DirectoryController.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use DI\Container;
|
||||
use PHLAK\Config\Config;
|
||||
use Slim\Psr7\Request;
|
||||
use Slim\Psr7\Response;
|
||||
use Slim\Views\Twig;
|
||||
use Symfony\Component\Finder\Exception\DirectoryNotFoundException;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
use Tightenco\Collect\Support\Collection;
|
||||
|
||||
class DirectoryController
|
||||
{
|
||||
/** @var Config App configuration component */
|
||||
protected $config;
|
||||
|
||||
/** @var Container Application container */
|
||||
protected $container;
|
||||
|
||||
/** @var Twig Twig templating component */
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* Create a new DirectoryController object.
|
||||
*
|
||||
* @param \DI\Container $container
|
||||
* @param \PHLAK\Config\Config $config
|
||||
* @param \Slim\Views\Twig $view
|
||||
*/
|
||||
public function __construct(Container $container, Config $config, Twig $view)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->config = $config;
|
||||
$this->view = $view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the DirectoryController.
|
||||
*
|
||||
* @param \Symfony\Component\Finder\Finder $files
|
||||
* @param \Slim\Psr7\Response $response
|
||||
* @param string $path
|
||||
*
|
||||
* @return \Psr\Http\Message\ResponseInterface
|
||||
*/
|
||||
public function __invoke(
|
||||
Finder $files,
|
||||
Request $request,
|
||||
Response $response,
|
||||
string $path = '.'
|
||||
) {
|
||||
$search = $request->getQueryParams()['search'] ?? null;
|
||||
|
||||
try {
|
||||
$files = $files->in($path);
|
||||
} catch (DirectoryNotFoundException $exception) {
|
||||
return $this->view->render($response->withStatus(404), '404.twig');
|
||||
}
|
||||
|
||||
return $this->view->render($response, 'index.twig', [
|
||||
'files' => $search ? $files->name(
|
||||
sprintf('/(?:.*)%s(?:.*)/i', preg_quote($search, '/'))
|
||||
) : $files->depth(0),
|
||||
'path' => $this->relativePath($path),
|
||||
'is_root' => $this->isRoot($path),
|
||||
'readme' => $this->readme($path),
|
||||
'search' => $search,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the relative path given a full path.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function relativePath(string $path): string
|
||||
{
|
||||
$realPath = realpath($this->container->get('base_path') . '/' . $path);
|
||||
|
||||
return Collection::make(explode('/', $realPath))->diff(
|
||||
explode('/', $this->container->get('base_path'))
|
||||
)->filter()->implode('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a provided path is the root path.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isRoot(string $path): bool
|
||||
{
|
||||
return realpath($path) === realpath($this->container->get('base_path'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the README file for a given path.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return \Symfony\Component\Finder\SplFileInfo|null
|
||||
*/
|
||||
protected function readme($path): ?SplFileInfo
|
||||
{
|
||||
if (! $this->config->get('app.display_readmes', true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$readmes = Finder::create()->in($path)->depth(0)->name('/^README(?:\..+)?$/i');
|
||||
$readmes->filter(function (SplFileInfo $file) {
|
||||
return (bool) preg_match('/text\/.+/', mime_content_type($file->getPathname()));
|
||||
});
|
||||
$readmes->sort(function (SplFileInfo $file1, SplFileInfo $file2) {
|
||||
return $file1->getExtension() <=> $file2->getExtension();
|
||||
});
|
||||
|
||||
if (! $readmes->hasResults()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$readmeArray = iterator_to_array($readmes);
|
||||
|
||||
return array_shift($readmeArray);
|
||||
}
|
||||
}
|
60
app/Controllers/FileInfoController.php
Normal file
60
app/Controllers/FileInfoController.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use DI\Container;
|
||||
use PHLAK\Config\Config;
|
||||
use Slim\Psr7\Response;
|
||||
use SplFileInfo;
|
||||
|
||||
class FileInfoController
|
||||
{
|
||||
/** @var Container The application container */
|
||||
protected $container;
|
||||
|
||||
/** @var Config App configuration component */
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Create a new FileInfoController object.
|
||||
*
|
||||
* @param \DI\Container $container
|
||||
* @param \PHLAK\Config\Config $config
|
||||
*/
|
||||
public function __construct(Container $container, Config $config)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the FileInfoController.
|
||||
*
|
||||
* @param \Slim\Psr7\Response $response
|
||||
* @param string $path
|
||||
*/
|
||||
public function __invoke(Response $response, string $path = '.')
|
||||
{
|
||||
$file = new SplFileInfo(
|
||||
realpath($this->container->get('base_path') . '/' . $path)
|
||||
);
|
||||
|
||||
if (! $file->isFile()) {
|
||||
return $response->withStatus(404, 'File not found');
|
||||
}
|
||||
|
||||
if ($file->getSize() >= $this->config->get('app.max_hash_size', 1000000000)) {
|
||||
return $response->withStatus(500, 'File size too large');
|
||||
}
|
||||
|
||||
$response->getBody()->write(json_encode([
|
||||
'hashes' => [
|
||||
'md5' => hash('md5', file_get_contents($file->getPathname())),
|
||||
'sha1' => hash('sha1', file_get_contents($file->getPathname())),
|
||||
'sha256' => hash('sha256', file_get_contents($file->getPathname())),
|
||||
]
|
||||
]));
|
||||
|
||||
return $response->withHeader('Content-Type', 'application/json');
|
||||
}
|
||||
}
|
32
app/Providers/ConfigProvider.php
Normal file
32
app/Providers/ConfigProvider.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use DI\Container;
|
||||
use PHLAK\Config\Config;
|
||||
|
||||
class ConfigProvider
|
||||
{
|
||||
/** @var Container The applicaiton container */
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* Create a new ConfigProvider object.
|
||||
*
|
||||
* @param \DI\Container $container
|
||||
*/
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize and register the Config component.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __invoke(): void
|
||||
{
|
||||
$this->container->set(Config::class, Config::createFromDirectory('app/config'));
|
||||
}
|
||||
}
|
107
app/Providers/FinderProvider.php
Normal file
107
app/Providers/FinderProvider.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\SortMethods;
|
||||
use Closure;
|
||||
use DI\Container;
|
||||
use PHLAK\Config\Config;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
use Tightenco\Collect\Support\Collection;
|
||||
|
||||
class FinderProvider
|
||||
{
|
||||
/** @const Application paths to be hidden */
|
||||
protected const APP_FILES = [
|
||||
'app', 'node_modules', 'vendor', 'mix-manifest.json', 'index.php'
|
||||
];
|
||||
|
||||
/** @const Array of sort options mapped to their respective methods */
|
||||
public const SORT_METHODS = [
|
||||
'accessed' => SortMethods\Accessed::class,
|
||||
'changed' => SortMethods\Changed::class,
|
||||
'modified' => SortMethods\Modified::class,
|
||||
'name' => SortMethods\Name::class,
|
||||
'natural' => SortMethods\Natural::class,
|
||||
'type' => SortMethods\Type::class,
|
||||
];
|
||||
|
||||
/** @var Config Application config */
|
||||
protected $config;
|
||||
|
||||
/** @var Container The application container */
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* Create a new ConfigProvider object.
|
||||
*
|
||||
* @param \DI\Container $container
|
||||
* @param \PHLAK\Config\Config $config
|
||||
*/
|
||||
public function __construct(Container $container, Config $config)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize and register the Finder component.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __invoke(): void
|
||||
{
|
||||
$finder = Finder::create()->followLinks();
|
||||
$finder->ignoreVCS($this->config->get('app.hide_vcs_files', false));
|
||||
$finder->filter(function (SplFileInfo $file) {
|
||||
foreach ($this->hiddenFiles() as $hiddenPath) {
|
||||
if (strpos($file->getRealPath(), $hiddenPath) === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
$sortOrder = $this->config->get('app.sort_order', 'type');
|
||||
if ($sortOrder instanceof Closure) {
|
||||
$finder->sort($sortOrder);
|
||||
} else {
|
||||
if (! array_key_exists($sortOrder, self::SORT_METHODS)) {
|
||||
throw new RuntimeException("Invalid sort option '{$sortOrder}'");
|
||||
}
|
||||
|
||||
$sortMethod = self::SORT_METHODS[$sortOrder];
|
||||
call_user_func(new $sortMethod, $finder);
|
||||
}
|
||||
|
||||
if ($this->config->get('app.reverse_sort', false)) {
|
||||
$finder->reverseSorting();
|
||||
}
|
||||
|
||||
$this->container->set(Finder::class, $finder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a collection of hidden files.
|
||||
*
|
||||
* @return \Tightenco\Collect\Support\Collection
|
||||
*/
|
||||
protected function hiddenFiles(): Collection
|
||||
{
|
||||
return Collection::make(
|
||||
$this->config->get('app.hidden_files', [])
|
||||
)->when($this->config->get('app.hide_app_files', true), function (Collection $collection) {
|
||||
return $collection->merge(self::APP_FILES);
|
||||
})->map(function (string $file) {
|
||||
return glob(
|
||||
$this->container->get('base_path') . '/' . $file,
|
||||
GLOB_BRACE | GLOB_NOSORT
|
||||
);
|
||||
})->flatten()->map(function (string $file) {
|
||||
return realpath($file);
|
||||
})->unique();
|
||||
}
|
||||
}
|
71
app/Providers/TwigProvider.php
Normal file
71
app/Providers/TwigProvider.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\ViewFunctions;
|
||||
use DI\Container;
|
||||
use PHLAK\Config\Config;
|
||||
use Slim\Views\Twig;
|
||||
use Twig\Extension\CoreExtension;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
class TwigProvider
|
||||
{
|
||||
/** @const Constant description */
|
||||
protected const VIEW_FUNCTIONS = [
|
||||
ViewFunctions\Asset::class,
|
||||
ViewFunctions\Breadcrumbs::class,
|
||||
ViewFunctions\Config::class,
|
||||
ViewFunctions\Icon::class,
|
||||
ViewFunctions\Markdown::class,
|
||||
ViewFunctions\ParentDir::class,
|
||||
ViewFunctions\SizeForHumans::class,
|
||||
];
|
||||
|
||||
/** @var Container The application container */
|
||||
protected $container;
|
||||
|
||||
/** @var Config Application config */
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Create a new ViewProvider object.
|
||||
*
|
||||
* @param \DI\Container $container
|
||||
* @param \PHLAK\Config\Config $config
|
||||
*/
|
||||
public function __construct(Container $container, Config $config)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize and register the Twig component.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __invoke(): void
|
||||
{
|
||||
$twig = new Twig(new FilesystemLoader('app/resources/views'));
|
||||
|
||||
$twig->getEnvironment()->setCache(
|
||||
$this->config->get('view.cache', 'app/cache/views')
|
||||
);
|
||||
|
||||
$twig->getEnvironment()->getExtension(CoreExtension::class)->setDateFormat(
|
||||
$this->config->get('app.date_format', 'Y-m-d H:i:s'), '%d days'
|
||||
);
|
||||
|
||||
foreach (self::VIEW_FUNCTIONS as $function) {
|
||||
$function = new $function($this->container, $this->config);
|
||||
|
||||
$twig->getEnvironment()->addFunction(
|
||||
new TwigFunction($function->name(), $function)
|
||||
);
|
||||
}
|
||||
|
||||
$this->container->set(Twig::class, $twig);
|
||||
}
|
||||
}
|
20
app/SortMethods/Accessed.php
Normal file
20
app/SortMethods/Accessed.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\SortMethods;
|
||||
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class Accessed extends SortMethod
|
||||
{
|
||||
/**
|
||||
* Sort by file accessed time.
|
||||
*
|
||||
* @param \Symfony\Component\Finder\Finder $finder
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __invoke(Finder $finder): void
|
||||
{
|
||||
$finder->sortByAccessedTime();
|
||||
}
|
||||
}
|
20
app/SortMethods/Changed.php
Normal file
20
app/SortMethods/Changed.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\SortMethods;
|
||||
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class Changed extends SortMethod
|
||||
{
|
||||
/**
|
||||
* Sory by file changed time.
|
||||
*
|
||||
* @param \Symfony\Component\Finder\Finder $finder
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __invoke(Finder $finder): void
|
||||
{
|
||||
$finder->sortByChangedTime();
|
||||
}
|
||||
}
|
20
app/SortMethods/Modified.php
Normal file
20
app/SortMethods/Modified.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\SortMethods;
|
||||
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class Modified extends SortMethod
|
||||
{
|
||||
/**
|
||||
* Sort by file modified time.
|
||||
*
|
||||
* @param \Symfony\Component\Finder\Finder $finder
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __invoke(Finder $finder): void
|
||||
{
|
||||
$finder->sortByModifiedTime();
|
||||
}
|
||||
}
|
20
app/SortMethods/Name.php
Normal file
20
app/SortMethods/Name.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\SortMethods;
|
||||
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class Name extends SortMethod
|
||||
{
|
||||
/**
|
||||
* Sort by file name.
|
||||
*
|
||||
* @param \Symfony\Component\Finder\Finder $finder
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __invoke(Finder $finder): void
|
||||
{
|
||||
$finder->sortByName();
|
||||
}
|
||||
}
|
20
app/SortMethods/Natural.php
Normal file
20
app/SortMethods/Natural.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\SortMethods;
|
||||
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class Natural extends SortMethod
|
||||
{
|
||||
/**
|
||||
* Sort by (natural) file name.
|
||||
*
|
||||
* @param \Symfony\Component\Finder\Finder $finder
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __invoke(Finder $finder): void
|
||||
{
|
||||
$finder->sortByName(true);
|
||||
}
|
||||
}
|
17
app/SortMethods/SortMethod.php
Normal file
17
app/SortMethods/SortMethod.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\SortMethods;
|
||||
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
abstract class SortMethod
|
||||
{
|
||||
/**
|
||||
* Run the sort method.
|
||||
*
|
||||
* @param \Symfony\Component\Finder\Finder $finder
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function __invoke(Finder $finder): void;
|
||||
}
|
20
app/SortMethods/Type.php
Normal file
20
app/SortMethods/Type.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\SortMethods;
|
||||
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class Type extends SortMethod
|
||||
{
|
||||
/**
|
||||
* Sory by file type.
|
||||
*
|
||||
* @param \Symfony\Component\Finder\Finder $finder
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __invoke(Finder $finder): void
|
||||
{
|
||||
$finder->sortByType();
|
||||
}
|
||||
}
|
34
app/Support/Helpers.php
Normal file
34
app/Support/Helpers.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
class Helpers
|
||||
{
|
||||
/**
|
||||
* Return the value of an environment vairable.
|
||||
*
|
||||
* @param string $envar The name of an environment variable
|
||||
* @param mixed $default Default value to return if no environment variable is set
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function env($envar, $default = null)
|
||||
{
|
||||
$value = getenv($envar);
|
||||
|
||||
if ($value === false) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
switch (strtolower($value)) {
|
||||
case 'true':
|
||||
return true;
|
||||
case 'false':
|
||||
return false;
|
||||
case 'null':
|
||||
return null;
|
||||
}
|
||||
|
||||
return preg_replace('/^"(.*)"$/', '$1', $value);
|
||||
}
|
||||
}
|
50
app/ViewFunctions/Asset.php
Normal file
50
app/ViewFunctions/Asset.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\ViewFunctions;
|
||||
|
||||
use Tightenco\Collect\Support\Collection;
|
||||
|
||||
class Asset extends ViewFunction
|
||||
{
|
||||
/** @const Constant description */
|
||||
protected const ASSET_PATH = '/app/dist/';
|
||||
|
||||
/** @var string The function name */
|
||||
protected $name = 'asset';
|
||||
|
||||
/**
|
||||
* Return the path to an asset.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __invoke(string $path): string
|
||||
{
|
||||
$assetPath = self::ASSET_PATH . $path;
|
||||
|
||||
if ($this->mixManifest()->has($assetPath)) {
|
||||
return $this->mixManifest()->get($assetPath);
|
||||
}
|
||||
|
||||
return $assetPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the mix manifest collection.
|
||||
*
|
||||
* @return \Tightenco\Collect\Support\Collection
|
||||
*/
|
||||
protected function mixManifest(): Collection
|
||||
{
|
||||
$mixManifest = $this->container->get('base_path') . '/mix-manifest.json';
|
||||
|
||||
if (! is_file($mixManifest)) {
|
||||
return new Collection();
|
||||
}
|
||||
|
||||
return Collection::make(
|
||||
json_decode(file_get_contents($mixManifest), true) ?? []
|
||||
);
|
||||
}
|
||||
}
|
33
app/ViewFunctions/Breadcrumbs.php
Normal file
33
app/ViewFunctions/Breadcrumbs.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\ViewFunctions;
|
||||
|
||||
use Tightenco\Collect\Support\Collection;
|
||||
|
||||
class Breadcrumbs extends ViewFunction
|
||||
{
|
||||
/** @var string The function name */
|
||||
protected $name = 'breadcrumbs';
|
||||
|
||||
/**
|
||||
* Build an array of breadcrumbs for a given path.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __invoke(string $path)
|
||||
{
|
||||
$breadcrumbs = Collection::make(explode('/', $path))->diff(
|
||||
explode('/', $this->container->get('base_path'))
|
||||
)->filter();
|
||||
|
||||
return $breadcrumbs->filter(function (string $crumb) {
|
||||
return $crumb !== '.';
|
||||
})->reduce(function (array $carry, string $crumb) {
|
||||
$carry[$crumb] = end($carry) . "/{$crumb}";
|
||||
|
||||
return $carry;
|
||||
}, []);
|
||||
}
|
||||
}
|
24
app/ViewFunctions/Config.php
Normal file
24
app/ViewFunctions/Config.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\ViewFunctions;
|
||||
|
||||
class Config extends ViewFunction
|
||||
{
|
||||
/** @var string The function name */
|
||||
protected $name = 'config';
|
||||
|
||||
/**
|
||||
* Retrieve an item from the view config.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $default
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function __invoke(string $key, $default = null)
|
||||
{
|
||||
$viewConfig = $this->config->split('view');
|
||||
|
||||
return $viewConfig->get($key, $default);
|
||||
}
|
||||
}
|
28
app/ViewFunctions/Icon.php
Normal file
28
app/ViewFunctions/Icon.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\ViewFunctions;
|
||||
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
|
||||
class Icon extends ViewFunction
|
||||
{
|
||||
/** @var string The function name */
|
||||
protected $name = 'icon';
|
||||
|
||||
/**
|
||||
* Retrieve the icon markup for a file.
|
||||
*
|
||||
* @param \Symfony\Component\Finder\SplFileInfo $file
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __invoke(SplFileInfo $file): string
|
||||
{
|
||||
$iconConfig = $this->config->split('icons');
|
||||
|
||||
$icon = $file->isDir() ? 'fas fa-folder'
|
||||
: $iconConfig->get($file->getExtension(), 'fas fa-file');
|
||||
|
||||
return "<i class=\"{$icon} fa-fw fa-lg\"></i>";
|
||||
}
|
||||
}
|
23
app/ViewFunctions/Markdown.php
Normal file
23
app/ViewFunctions/Markdown.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\ViewFunctions;
|
||||
|
||||
use Parsedown;
|
||||
|
||||
class Markdown extends ViewFunction
|
||||
{
|
||||
/** @var string The function name */
|
||||
protected $name = 'markdown';
|
||||
|
||||
/**
|
||||
* Parses a string of markdown into HTML.
|
||||
*
|
||||
* @param string $string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __invoke(string $string)
|
||||
{
|
||||
return Parsedown::instance()->parse($string);
|
||||
}
|
||||
}
|
25
app/ViewFunctions/ParentDir.php
Normal file
25
app/ViewFunctions/ParentDir.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\ViewFunctions;
|
||||
|
||||
use Tightenco\Collect\Support\Collection;
|
||||
|
||||
class ParentDir extends ViewFunction
|
||||
{
|
||||
/** @var string The function name */
|
||||
protected $name = 'parent_dir';
|
||||
|
||||
/**
|
||||
* Get the parent directory for a given path.
|
||||
*
|
||||
* @param string $path
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __invoke(string $path)
|
||||
{
|
||||
return Collection::make(
|
||||
explode('/', $path)
|
||||
)->filter()->slice(0, -1)->implode('/');
|
||||
}
|
||||
}
|
26
app/ViewFunctions/SizeForHumans.php
Normal file
26
app/ViewFunctions/SizeForHumans.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\ViewFunctions;
|
||||
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
|
||||
class SizeForHumans extends ViewFunction
|
||||
{
|
||||
/** @var string The function name */
|
||||
protected $name = 'sizeForHumans';
|
||||
|
||||
/**
|
||||
* Get the human readable file size from a file object.
|
||||
*
|
||||
* @param SplFileInfo $file A file object
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __invoke(SplFileInfo $file): string
|
||||
{
|
||||
$sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
$factor = (int) floor((strlen((string) $file->getSize()) - 1) / 3);
|
||||
|
||||
return sprintf('%.2f', $file->getSize() / pow(1024, $factor)) . $sizes[$factor];
|
||||
}
|
||||
}
|
39
app/ViewFunctions/ViewFunction.php
Normal file
39
app/ViewFunctions/ViewFunction.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\ViewFunctions;
|
||||
|
||||
use DI\Container;
|
||||
use PHLAK\Config\Config;
|
||||
|
||||
abstract class ViewFunction
|
||||
{
|
||||
/** @var string The function name */
|
||||
protected $name = '';
|
||||
|
||||
/** @var Container The application container */
|
||||
protected $container;
|
||||
|
||||
/** @var Config App configuration component */
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Create a new ViewFunction object.
|
||||
*
|
||||
* @param \PHLAK\Config\Config $config
|
||||
*/
|
||||
public function __construct(Container $container, Config $config)
|
||||
{
|
||||
$this->container = $container;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the function name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function name(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
2
app/cache/.gitignore
vendored
Normal file
2
app/cache/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
68
app/config/app.php
Normal file
68
app/config/app.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
use App\Support\Helpers;
|
||||
|
||||
return [
|
||||
/**
|
||||
* Sorting order of files and folders.
|
||||
*
|
||||
* Possible values: type, natural, name, accessed, changed, modified
|
||||
* Default value: type
|
||||
*/
|
||||
'sort_order' => Helpers::env('SORT_ORDER'),
|
||||
|
||||
/**
|
||||
* Reverse the sort order.
|
||||
*
|
||||
* Default value: false
|
||||
*/
|
||||
'reverse_sort' => Helpers::env('REVERSE_SORT'),
|
||||
|
||||
/**
|
||||
* Array of files that will be hidden from the listing.
|
||||
* Supports glob patterns.
|
||||
*
|
||||
* Default value: []
|
||||
*/
|
||||
'hidden_files' => [
|
||||
// ...
|
||||
],
|
||||
|
||||
/**
|
||||
* Whether or not to hide application files/directories form the listing.
|
||||
*
|
||||
* Default value: true
|
||||
*/
|
||||
'hide_app_files' => Helpers::env('HIDE_APP_FILES'),
|
||||
|
||||
/**
|
||||
* Hide version control system files (e.g. .git directories) from listing.
|
||||
*
|
||||
* Default value: true
|
||||
*/
|
||||
'hide_vcs_files' => Helpers::env('HIDE_VSC_FILES'),
|
||||
|
||||
/**
|
||||
* Parse and render README files on the page.
|
||||
*
|
||||
* Default value: true
|
||||
*/
|
||||
'display_readmes' => Helpers::env('DISPLAY_READMES'),
|
||||
|
||||
/**
|
||||
* The maximum file size (in bytes) that can be hashed. This helps to
|
||||
* prevent timeouts for excessively large files.
|
||||
*
|
||||
* Defualt value: 1000000000
|
||||
*/
|
||||
'max_hash_size' => Helpers::env('MAX_HASH_SIZE'),
|
||||
|
||||
/**
|
||||
* Additional providers to be loaded during application initialization.
|
||||
*
|
||||
* Default value: []
|
||||
*/
|
||||
'providers' => [
|
||||
// ...
|
||||
],
|
||||
];
|
131
app/config/icons.php
Normal file
131
app/config/icons.php
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// Images
|
||||
'ai' => 'fas fa-image',
|
||||
'bmp' => 'fas fa-image',
|
||||
'eps' => 'fas fa-image',
|
||||
'gif' => 'fas fa-image',
|
||||
'jpg' => 'fas fa-image',
|
||||
'jpeg' => 'fas fa-image',
|
||||
'png' => 'fas fa-image',
|
||||
'ps' => 'fas fa-image',
|
||||
'psd' => 'fas fa-image',
|
||||
'svg' => 'fas fa-image',
|
||||
'tga' => 'fas fa-image',
|
||||
'tif' => 'fas fa-image',
|
||||
'drw' => 'fas fa-image',
|
||||
|
||||
// Data
|
||||
'csv' => 'fas fa-file-csv',
|
||||
'json' => 'fas fa-file-alt',
|
||||
'yaml' => 'fas fa-file-alt',
|
||||
|
||||
// Code
|
||||
'c' => 'fas fa-code',
|
||||
'class' => 'fas fa-code',
|
||||
'cpp' => 'fas fa-code',
|
||||
'css' => 'fab fab fa-css3',
|
||||
'erb' => 'fas fa-code',
|
||||
'htm' => 'fab fa-html5',
|
||||
'html' => 'fab fa-html5',
|
||||
'java' => 'fab fa-java',
|
||||
'js' => 'fab fa-js',
|
||||
'php' => 'fab fa-php',
|
||||
'pl' => 'fas fa-code',
|
||||
'py' => 'fab fa-python',
|
||||
'rb' => 'fas fa-code',
|
||||
'xhtml' => 'fas fa-code',
|
||||
'xml' => 'fas fa-code',
|
||||
|
||||
// Text and Markup
|
||||
'cfg' => 'fas fa-file-alt',
|
||||
'ini' => 'fas fa-file-alt',
|
||||
'log' => 'fas fa-file-alt',
|
||||
'md' => 'fab fa-markdown',
|
||||
'rtf' => 'fas fa-file-alt',
|
||||
'txt' => 'fas fa-file-alt',
|
||||
|
||||
// Documents
|
||||
'doc' => 'fas fa-file-word',
|
||||
'docx' => 'fas fa-file-word',
|
||||
'odt' => 'fas fa-file-alt',
|
||||
'pdf' => 'fas fa-file-pdf',
|
||||
'ppt' => 'fas fa-file-powerpoint',
|
||||
'pptx' => 'fas fa-file-powerpoint',
|
||||
'xls' => 'fas fa-file-excel',
|
||||
'xlsx' => 'fas fa-file-excel',
|
||||
|
||||
// Archives
|
||||
'7z' => 'fas fa-file-archive',
|
||||
'bz' => 'fas fa-file-archive',
|
||||
'gz' => 'fas fa-file-archive',
|
||||
'rar' => 'fas fa-file-archive',
|
||||
'tar' => 'fas fa-file-archive',
|
||||
'zip' => 'fas fa-file-archive',
|
||||
|
||||
// Audio
|
||||
'aac' => 'fas fa-music',
|
||||
'flac' => 'fas fa-music',
|
||||
'mid' => 'fas fa-music',
|
||||
'midi' => 'fas fa-music',
|
||||
'mp3' => 'fas fa-music',
|
||||
'ogg' => 'fas fa-music',
|
||||
'wma' => 'fas fa-music',
|
||||
'wav' => 'fas fa-music',
|
||||
|
||||
// Databases
|
||||
'accdb' => 'fas fa-database',
|
||||
'db' => 'fas fa-database',
|
||||
'dbf' => 'fas fa-database',
|
||||
'mdb' => 'fas fa-database',
|
||||
'pdb' => 'fas fa-database',
|
||||
'sql' => 'fas fa-database',
|
||||
|
||||
// Executables
|
||||
'app' => 'fas fa-window',
|
||||
'com' => 'fas fa-window',
|
||||
'exe' => 'fas fa-window',
|
||||
'jar' => 'fas fa-window',
|
||||
'msi' => 'fas fa-window',
|
||||
'vb' => 'fas fa-window',
|
||||
|
||||
// Fonts
|
||||
'eot' => 'fas fa-font-case',
|
||||
'otf' => 'fas fa-font-case',
|
||||
'ttf' => 'fas fa-font-case',
|
||||
'woff' => 'fas fa-font-case',
|
||||
|
||||
// Game Files
|
||||
'gam' => 'fas fa-gamepad',
|
||||
'nes' => 'fas fa-gamepad',
|
||||
'rom' => 'fas fa-gamepad',
|
||||
'sav' => 'fas fa-save',
|
||||
|
||||
// Package Files
|
||||
'box' => 'fas fa-archive',
|
||||
'deb' => 'fas fa-archive',
|
||||
'rpm' => 'fas fa-archive',
|
||||
|
||||
// Scripts
|
||||
'bat' => 'fas fa-terminal',
|
||||
'cmd' => 'fas fa-terminal',
|
||||
'sh' => 'fas fa-terminal',
|
||||
|
||||
// Video
|
||||
'avi' => 'fas fa-video',
|
||||
'flv' => 'fas fa-video',
|
||||
'mkv' => 'fas fa-video',
|
||||
'mov' => 'fas fa-video',
|
||||
'mp4' => 'fas fa-video',
|
||||
'mpg' => 'fas fa-video',
|
||||
'ogv' => 'fas fa-video',
|
||||
'webm' => 'fas fa-video',
|
||||
'wmv' => 'fas fa-video',
|
||||
'swf' => 'fas fa-video',
|
||||
|
||||
// Miscellaneous
|
||||
'bak' => 'fas fa-save',
|
||||
'lock' => 'fas fa-lock',
|
||||
'msg' => 'fas fa-envelope',
|
||||
];
|
28
app/config/view.php
Normal file
28
app/config/view.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use App\Support\Helpers;
|
||||
|
||||
return [
|
||||
/**
|
||||
* Enable dark mode?
|
||||
*
|
||||
* Default value: false
|
||||
*/
|
||||
'dark_mode' => Helpers::env('DARK_MODE'),
|
||||
|
||||
/**
|
||||
* Default date format. For additional info on date formatting see:
|
||||
* https://www.php.net/manual/en/function.date.php.
|
||||
*
|
||||
* Default value: 'Y-m-d H:i:s'
|
||||
*/
|
||||
'date_format' => Helpers::env('DATE_FORMAT'),
|
||||
|
||||
/**
|
||||
* Path to the view cache directory.
|
||||
* Set to 'false' to disable view caching entirely.
|
||||
*
|
||||
* Default value: 'app/cache/views'
|
||||
*/
|
||||
'cache' => Helpers::env('VIEW_CACHE'),
|
||||
];
|
BIN
app/resources/images/favicon.dark.png
Normal file
BIN
app/resources/images/favicon.dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
BIN
app/resources/images/favicon.light.png
Normal file
BIN
app/resources/images/favicon.light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
29
app/resources/js/app.js
Normal file
29
app/resources/js/app.js
Normal file
@@ -0,0 +1,29 @@
|
||||
window.Vue = require('vue');
|
||||
|
||||
Vue.component('file-info-modal', require('./components/file-info-modal.vue').default);
|
||||
|
||||
const app = new Vue({
|
||||
el: "#app",
|
||||
data: function() {
|
||||
return {
|
||||
search: ''
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
showFileInfo(filePath) {
|
||||
this.$refs.fileInfoModal.show(filePath);
|
||||
}
|
||||
},
|
||||
beforeMount: function() {
|
||||
this.search = this.$el.querySelector('input[name="search"]').value;
|
||||
}
|
||||
});
|
||||
|
||||
let link = document.getElementById('scroll-to-top');
|
||||
window.addEventListener('scroll', function() {
|
||||
if (window.scrollY > 10) {
|
||||
link.classList.remove('hidden');
|
||||
} else {
|
||||
link.classList.add('hidden');
|
||||
}
|
||||
});
|
87
app/resources/js/components/file-info-modal.vue
Normal file
87
app/resources/js/components/file-info-modal.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div id="file-info-modal"
|
||||
class="fixed top-0 flex justify-center items-center w-screen h-screen p-4 z-50"
|
||||
style="background-color: hsla(218, 23%, 23%, 0.5)"
|
||||
v-bind:class="this.styles"
|
||||
v-on:click.self="hide()"
|
||||
>
|
||||
<div id="file-info-dialogue" class="bg-white rounded-lg shadow-lg overflow-hidden" v-show="! loading">
|
||||
<header class="flex justify-between items-center bg-blue-600 p-4">
|
||||
<i class="fas fa-info-circle fa-lg text-white"></i>
|
||||
|
||||
<div class="items-center text-xl text-white font-mono mx-4">
|
||||
{{ title }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="flex justify-center items-center rounded-full w-6 h-6 text-blue-900 text-sm hover:bg-red-700 hover:text-white hover:shadow"
|
||||
v-on:click="hide()"
|
||||
>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<content class="flex justify-center items-center p-4">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table-auto">
|
||||
<tbody>
|
||||
<tr v-for="(hash, title) in this.hashes" v-bind:key="hash">
|
||||
<td class="border font-bold px-4 py-2">{{ title }}</td>
|
||||
<td class="border font-mono px-4 py-2">{{ hash }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</content>
|
||||
</div>
|
||||
|
||||
<i class="fas fa-spinner fa-pulse fa-5x text-white" v-show="loading"></i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const axios = require('axios').default;
|
||||
|
||||
export default {
|
||||
data: function () {
|
||||
return {
|
||||
filePath: 'file-info.txt',
|
||||
hashes: {
|
||||
'md5': '••••••••••••••••••••••••••••••••',
|
||||
'sha1': '••••••••••••••••••••••••••••••••••••••••',
|
||||
'sha256': '••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••'
|
||||
},
|
||||
loading: true,
|
||||
visible: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
styles() {
|
||||
return { 'hidden': ! this.visible };
|
||||
},
|
||||
title() {
|
||||
return this.filePath.split('/').pop();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async show(filePath) {
|
||||
this.filePath = filePath;
|
||||
this.visible = true;
|
||||
|
||||
await axios.get('/file-info/' + filePath).then(function (response) {
|
||||
this.hashes = response.data.hashes;
|
||||
this.loading = false;
|
||||
}.bind(this)).catch(
|
||||
response => this.hide() && console.error(response)
|
||||
);
|
||||
},
|
||||
hide() {
|
||||
this.visible = false;
|
||||
this.loading = true;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('keyup', e => e.keyCode == 27 && this.hide());
|
||||
}
|
||||
}
|
||||
</script>
|
17
app/resources/sass/app.scss
Normal file
17
app/resources/sass/app.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
// Tailwind
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
// Icons
|
||||
$fa-font-path: "./webfonts";
|
||||
@import "~@fortawesome/fontawesome-free/scss/fontawesome.scss";
|
||||
@import "~@fortawesome/fontawesome-free/scss/brands.scss";
|
||||
@import "~@fortawesome/fontawesome-free/scss/solid.scss";
|
||||
|
||||
// Fonts
|
||||
@import url("https://fonts.googleapis.com/css?family=Source+Code+Pro|Work+Sans:200,400&display=swap");
|
||||
|
||||
// Additional Styles
|
||||
@import "markdown.scss";
|
||||
@import "dark-mode.scss";
|
104
app/resources/sass/dark-mode.scss
Normal file
104
app/resources/sass/dark-mode.scss
Normal file
@@ -0,0 +1,104 @@
|
||||
#app.dark-mode {
|
||||
@apply bg-gray-800;
|
||||
|
||||
header {
|
||||
@apply bg-purple-700;
|
||||
|
||||
#search {
|
||||
@apply bg-purple-800;
|
||||
|
||||
i.fa-search {
|
||||
@apply text-purple-900;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply text-purple-900;
|
||||
|
||||
&:hover {
|
||||
@apply text-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#content {
|
||||
@apply text-white;
|
||||
}
|
||||
|
||||
#file-list a:hover {
|
||||
@apply bg-purple-700;
|
||||
|
||||
button:hover {
|
||||
@apply bg-purple-900;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
@apply text-white;
|
||||
@apply border-white;
|
||||
|
||||
a:hover {
|
||||
@apply text-purple-600;
|
||||
}
|
||||
}
|
||||
|
||||
#scroll-to-top {
|
||||
@apply bg-purple-700;
|
||||
}
|
||||
|
||||
#file-info-modal {
|
||||
background-color: hsla(218, 23%, 33%, 0.5) !important;
|
||||
|
||||
#file-info-dialogue {
|
||||
@apply bg-gray-800;
|
||||
@apply text-white;
|
||||
|
||||
header {
|
||||
@apply bg-purple-700;
|
||||
|
||||
button {
|
||||
@apply text-purple-900;
|
||||
|
||||
&:hover {
|
||||
@apply text-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#readme {
|
||||
article {
|
||||
@apply bg-gray-900;
|
||||
@apply border-0;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown {
|
||||
a {
|
||||
@apply text-purple-600;
|
||||
|
||||
&:hover {
|
||||
@apply text-purple-800;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
@apply border-gray-800;
|
||||
}
|
||||
|
||||
h6 {
|
||||
@apply text-base;
|
||||
@apply text-gray-600;
|
||||
}
|
||||
|
||||
code {
|
||||
@apply bg-gray-800;
|
||||
}
|
||||
|
||||
hr {
|
||||
@apply border-gray-800;
|
||||
}
|
||||
}
|
||||
}
|
114
app/resources/sass/markdown.scss
Normal file
114
app/resources/sass/markdown.scss
Normal file
@@ -0,0 +1,114 @@
|
||||
.markdown {
|
||||
@apply break-words;
|
||||
|
||||
a {
|
||||
@apply text-blue-600;
|
||||
@apply underline;
|
||||
|
||||
&:hover {
|
||||
@apply text-blue-800;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
@apply inline;
|
||||
}
|
||||
|
||||
p,
|
||||
blockquote,
|
||||
ul,
|
||||
ol,
|
||||
dl,
|
||||
table,
|
||||
pre {
|
||||
@apply my-4;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
@apply pl-8;
|
||||
}
|
||||
|
||||
ul {
|
||||
@apply list-disc;
|
||||
}
|
||||
|
||||
ol {
|
||||
@apply list-decimal;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-4xl;
|
||||
@apply border-b;
|
||||
@apply border-gray-400;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-3xl;
|
||||
@apply border-b;
|
||||
@apply border-gray-400;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-2xl;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-xl;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@apply text-base;
|
||||
}
|
||||
|
||||
h6 {
|
||||
@apply text-base;
|
||||
@apply text-gray-600;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply font-bold;
|
||||
@apply leading-relaxed;
|
||||
@apply mt-8;
|
||||
@apply mb-4;
|
||||
}
|
||||
|
||||
h1 + p,
|
||||
h2 + p,
|
||||
h3 + p {
|
||||
@apply mt-4;
|
||||
}
|
||||
|
||||
code {
|
||||
@apply bg-gray-200;
|
||||
@apply rounded-sm;
|
||||
@apply p-1;
|
||||
@apply font-mono;
|
||||
@apply overflow-y-auto;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
@apply block;
|
||||
@apply p-4;
|
||||
@apply text-sm;
|
||||
@apply whitespace-pre;
|
||||
}
|
||||
|
||||
hr {
|
||||
@apply border-t-2;
|
||||
@apply border-gray-400;
|
||||
}
|
||||
|
||||
& > :first-child {
|
||||
@apply mt-0;
|
||||
}
|
||||
|
||||
& > :last-child {
|
||||
@apply mb-0;
|
||||
}
|
||||
}
|
13
app/resources/views/404.twig
Normal file
13
app/resources/views/404.twig
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends 'layouts/app.twig' %}
|
||||
|
||||
{% block content %}
|
||||
{% include 'components/header.twig' %}
|
||||
|
||||
<div id="content" class="flex-grow container flex justify-center items-center mx-auto px-4">
|
||||
<p class="font-thin text-4xl text-gray-600">
|
||||
404 • Not Found
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% include 'components/footer.twig' %}
|
||||
{% endblock %}
|
42
app/resources/views/components/file.twig
Normal file
42
app/resources/views/components/file.twig
Normal file
@@ -0,0 +1,42 @@
|
||||
<a
|
||||
href="/{{ parentDir ? parent_dir(path) : file.getPathname }}"
|
||||
class="flex flex-col items-center rounded-lg font-mono group hover:bg-gray-200 hover:shadow"
|
||||
>
|
||||
<div class="flex justify-between items-center p-4 w-full">
|
||||
<div class="pr-2">
|
||||
{% if parentDir %}
|
||||
<i class="fas fa-level-up-alt fa-fw fa-lg"></i>
|
||||
{% else %}
|
||||
{{ icon(file) | raw }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex-1 truncate">
|
||||
{{ parentDir ? '..' : file.getRelativePathname }}
|
||||
</div>
|
||||
|
||||
{% if file.isFile %}
|
||||
<div class="ml-2">
|
||||
<button
|
||||
title="File Info"
|
||||
class="flex justify-center items-center rounded-full p-2 -m-1 md:invisible hover:bg-gray-400 hover:shadow group-hover:visible"
|
||||
v-on:click.prevent="showFileInfo('{{ file.getPathname }}')"
|
||||
>
|
||||
<i class="fas fa-info-circle"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="hidden whitespace-no-wrap text-right mx-2 w-1/6 sm:block">
|
||||
{% if parentDir or file.isDir %}
|
||||
—
|
||||
{% else %}
|
||||
{{ sizeForHumans(file) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="hidden whitespace-no-wrap text-right truncate ml-2 w-1/4 sm:block">
|
||||
{{ parentDir ? '—' : file.getMTime | date }}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
21
app/resources/views/components/footer.twig
Normal file
21
app/resources/views/components/footer.twig
Normal file
@@ -0,0 +1,21 @@
|
||||
<footer class="container border-t-2 border-gray-800 text-center mx-auto px-4 py-8">
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<p class="mb-4">
|
||||
Powered by <a href="https://www.directorylister.com" class="underline hover:text-blue-700">Directory Lister</a>
|
||||
</p>
|
||||
|
||||
<div class="flex">
|
||||
<a href="https://github.com/DirectoryLister/DirectoryLister" title="GitHub" class="text-gray-400 mx-2 hover:text-github">
|
||||
<i class="fab fa-github fa-lg"></i>
|
||||
</a>
|
||||
|
||||
<a href="https://twitter.com/DirectoryLister" title="Twitter" class="text-gray-400 mx-2 hover:text-twitter">
|
||||
<i class="fab fa-twitter fa-lg"></i>
|
||||
</a>
|
||||
|
||||
<a href="https://spectrum.chat/directory-lister" title="Spectrum" class="text-gray-400 mx-2 hover:text-spectrum">
|
||||
<i class="fas fa-comments fa-lg"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
32
app/resources/views/components/header.twig
Normal file
32
app/resources/views/components/header.twig
Normal file
@@ -0,0 +1,32 @@
|
||||
<header id="header" class="bg-blue-600 shadow sticky top-0">
|
||||
<div class="container flex flex-col justify-between items-center mx-auto p-4 md:flex-row">
|
||||
<div class="font-mono text-white text-sm tracking-tight mb-2 md:my-1">
|
||||
<a href="/" class="hover:underline">Home</a>
|
||||
|
||||
{% if path %}
|
||||
{% for name, path in breadcrumbs(path) %}
|
||||
/ <a href="{{ path }}" class="hover:underline">{{ name }}</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form action="." method="get" id="search" class="group relative block w-full bg-blue-700 rounded-full shadow-inner md:w-4/12 md:-my-2">
|
||||
<input type="text" name="search" placeholder="Search this directory..." value="{{ search }}"
|
||||
class="bg-transparent placeholder-gray-900 text-white w-full px-10 py-2"
|
||||
v-model="search"
|
||||
>
|
||||
|
||||
<div class="flex items-center absolute left-0 inset-y-0 ml-2 pointer-events-none">
|
||||
<div class="flex justify-center items-center text-blue-900 w-6 h-6">
|
||||
<i class="fas fa-search fa-fw"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center absolute right-0 inset-y-0 mr-2" v-show="search">
|
||||
<a href="." class="flex justify-center items-center rounded-full text-blue-900 w-6 h-6 hover:bg-red-700 hover:text-white hover:shadow">
|
||||
<i class="fas fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</header>
|
13
app/resources/views/components/readme.twig
Normal file
13
app/resources/views/components/readme.twig
Normal file
@@ -0,0 +1,13 @@
|
||||
<div id="readme" class="rounded-lg overflow-hidden shadow-md my-8">
|
||||
<header class="flex items-center bg-blue-600 px-4 py-3 text-white">
|
||||
<i class="fas fa-book fa-lg pr-3"></i> README.md
|
||||
</header>
|
||||
|
||||
<article class="bg-gray-100 rounded-b-lg px-4 py-8 sm:px-6 md:px-8 lg:px-12 {{ readme.getExtension == 'md' ? 'markdown' : 'font-mono' }}">
|
||||
{% if readme.getExtension == 'md' %}
|
||||
{{ markdown(readme.getContents) | raw }}
|
||||
{% else %}
|
||||
{{ readme.getContents | nl2br }}
|
||||
{% endif %}
|
||||
</article>
|
||||
</div>
|
10
app/resources/views/components/scroll-to-top.twig
Normal file
10
app/resources/views/components/scroll-to-top.twig
Normal file
@@ -0,0 +1,10 @@
|
||||
<div class="fixed bottom-0 left-0 right-0 pointer-events-none">
|
||||
<div class="container flex justify-end mx-auto px-4 py-10">
|
||||
<a id="scroll-to-top"
|
||||
class="flex justify-center items-center w-12 h-12 right-0 rounded-full shadow-lg bg-blue-600 text-white cursor-pointer pointer-events-auto hover:bg-blue-700 hidden"
|
||||
onclick="window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });" title="Scroll to Top"
|
||||
>
|
||||
<i class="fas fa-arrow-up fa-lg"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
46
app/resources/views/index.twig
Normal file
46
app/resources/views/index.twig
Normal file
@@ -0,0 +1,46 @@
|
||||
{% extends "layouts/app.twig" %}
|
||||
|
||||
{% block content %}
|
||||
{% include "components/header.twig" %}
|
||||
|
||||
<div id="content" class="flex-grow container mx-auto px-4">
|
||||
<div class="my-4">
|
||||
<div class="flex justify-between font-bold p-4">
|
||||
<div class="flex-grow font-mono mr-2">
|
||||
File Name
|
||||
</div>
|
||||
|
||||
<div class="font-mono text-right w-1/6 mx-2 hidden sm:block">
|
||||
Size
|
||||
</div>
|
||||
|
||||
<div class="font-mono text-right w-1/4 ml-2 hidden sm:block">
|
||||
Date
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul id="file-list">
|
||||
<li>
|
||||
{% if not search and not is_root %}
|
||||
{{ include('components/file.twig', { parentDir: true }) }}
|
||||
{% endif %}
|
||||
</li>
|
||||
|
||||
<li>
|
||||
{% for file in files %}
|
||||
{{ include('components/file.twig') }}
|
||||
{% endfor %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% if readme and not search %}
|
||||
{% include 'components/readme.twig' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% include 'components/footer.twig' %}
|
||||
|
||||
{% include 'components/scroll-to-top.twig' %}
|
||||
<file-info-modal ref="fileInfoModal"></file-info-modal>
|
||||
{% endblock %}
|
14
app/resources/views/layouts/app.twig
Normal file
14
app/resources/views/layouts/app.twig
Normal file
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="/app/resources/images/favicon.{{ config('dark_mode') ? 'dark' : 'light' }}.png">
|
||||
<link rel="stylesheet" href="{{ asset('app.css') }}">
|
||||
|
||||
<title>{{ path | default('Home') }} • Directory Lister</title>
|
||||
|
||||
<div id="app" class="flex flex-col min-h-screen font-sans {{ config('dark_mode') ? 'dark-mode' : 'light-mode' }}">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<script src="{{ asset('app.js') }}" defer></script>
|
9
artifact.files
Normal file
9
artifact.files
Normal file
@@ -0,0 +1,9 @@
|
||||
app
|
||||
node_modules
|
||||
vendor
|
||||
.env.example
|
||||
directory-lister.svg
|
||||
LICENSE
|
||||
mix-manifest.json
|
||||
README.md
|
||||
index.php
|
9
artifacts.include
Normal file
9
artifacts.include
Normal file
@@ -0,0 +1,9 @@
|
||||
app
|
||||
node_modules
|
||||
vendor
|
||||
.env.example
|
||||
directory-lister.svg
|
||||
LICENSE
|
||||
mix-manifest.json
|
||||
README.md
|
||||
index.php
|
2
artifacts/.gitignore
vendored
Normal file
2
artifacts/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
45
composer.json
Normal file
45
composer.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "phlak/directory-lister",
|
||||
"description": "PHP directory lister",
|
||||
"type": "project",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Chris Kankiewicz",
|
||||
"email": "Chris@ChrisKankiewicz.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.2",
|
||||
"erusev/parsedown": "^1.7",
|
||||
"phlak/config": "^6.0",
|
||||
"php-di/php-di": "^6.0",
|
||||
"php-di/slim-bridge": "^3.0",
|
||||
"slim/psr7": "^1.0",
|
||||
"slim/slim": "^4.3",
|
||||
"slim/twig-view": "3.0.0",
|
||||
"symfony/finder": "^5.0",
|
||||
"tightenco/collect": "^6.4",
|
||||
"vlucas/phpdotenv": "^4.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^2.3",
|
||||
"phpunit/phpunit": "^7.0 || ^8.0",
|
||||
"psy/psysh": "^0.9.9",
|
||||
"vimeo/psalm": "^3.6"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true,
|
||||
"optimize-autoloader": true
|
||||
}
|
||||
}
|
5114
composer.lock
generated
Normal file
5114
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
directory-lister.svg
Normal file
1
directory-lister.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 16 KiB |
37
docker-compose.yaml
Normal file
37
docker-compose.yaml
Normal file
@@ -0,0 +1,37 @@
|
||||
version: '3.4'
|
||||
|
||||
services:
|
||||
apache:
|
||||
container_name: directory-lister-apache
|
||||
build:
|
||||
context: .docker
|
||||
dockerfile: Dockerfile.apache
|
||||
environment:
|
||||
VIRTUAL_HOST: directory-lister.local,directory-lister.apache.local
|
||||
volumes:
|
||||
- ./:/var/www/html
|
||||
restart: unless-stopped
|
||||
|
||||
nginx:
|
||||
container_name: directory-lister-nginx
|
||||
image: nginx:1.17
|
||||
environment:
|
||||
VIRTUAL_HOST: directory-lister.nginx.local
|
||||
volumes:
|
||||
- ./.docker/nginx/config/default.conf:/etc/nginx/conf.d/default.conf
|
||||
- ./:/var/www/html
|
||||
restart: unless-stopped
|
||||
|
||||
php-fpm:
|
||||
container_name: directory-lister-php-fpm
|
||||
build:
|
||||
context: .docker
|
||||
dockerfile: Dockerfile.nginx
|
||||
volumes:
|
||||
- ./:/var/www/html
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
name: development
|
28
index.php
Normal file
28
index.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use App\Bootstrap\AppManager;
|
||||
use App\Controllers;
|
||||
use DI\Container;
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
require __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
// Set file access restrictions
|
||||
ini_set('open_basedir', __DIR__);
|
||||
|
||||
// Initialize environment variable handler
|
||||
Dotenv::createImmutable(__DIR__)->safeLoad();
|
||||
|
||||
// Initialize the container
|
||||
$container = new Container();
|
||||
$container->set('base_path', __DIR__);
|
||||
|
||||
// Configure the application
|
||||
$app = $container->call(AppManager::class);
|
||||
|
||||
// Register routes
|
||||
$app->get('/file-info/[{path:.*}]', Controllers\FileInfoController::class);
|
||||
$app->get('/[{path:.*}]', Controllers\DirectoryController::class);
|
||||
|
||||
// Enagage!
|
||||
$app->run();
|
10212
package-lock.json
generated
Normal file
10212
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
package.json
Normal file
27
package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"author": "Chris Kankiewicz",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "npm run development",
|
||||
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
|
||||
"watch": "npm run development -- --watch",
|
||||
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
|
||||
"prod": "npm run production",
|
||||
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.19.1",
|
||||
"vue": "^2.6.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.12.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"laravel-mix": "^5.0.1",
|
||||
"laravel-mix-purgecss": "^4.2.0",
|
||||
"resolve-url-loader": "^3.1.1",
|
||||
"sass": "^1.25.0",
|
||||
"sass-loader": "^8.0.2",
|
||||
"tailwindcss": "^1.1.4",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
}
|
||||
}
|
16
phpunit.xml
Normal file
16
phpunit.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit bootstrap="vendor/autoload.php" colors="true">
|
||||
<testsuites>
|
||||
<testsuite name="Directory Lister Test Suite">
|
||||
<directory suffix="Test.php">tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<filter>
|
||||
<whitelist processUncoveredFilesFromWhitelist="true">
|
||||
<directory suffix=".php">app/</directory>
|
||||
<exclude>
|
||||
<directory>app/cache</directory>
|
||||
</exclude>
|
||||
</whitelist>
|
||||
</filter>
|
||||
</phpunit>
|
2
psalm-baseline.xml
Normal file
2
psalm-baseline.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<files psalm-version="3.8.3@389af1bfc739bfdff3f9e3dc7bd6499aee51a831"/>
|
63
psalm.xml
Normal file
63
psalm.xml
Normal file
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0"?>
|
||||
<psalm
|
||||
totallyTyped="false"
|
||||
resolveFromConfigFile="true"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="https://getpsalm.org/schema/config"
|
||||
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
|
||||
errorBaseline="psalm-baseline.xml"
|
||||
>
|
||||
<projectFiles>
|
||||
<directory name="app" />
|
||||
<ignoreFiles>
|
||||
<directory name="vendor" />
|
||||
</ignoreFiles>
|
||||
</projectFiles>
|
||||
|
||||
<issueHandlers>
|
||||
<UndefinedInterfaceMethod>
|
||||
<errorLevel type="suppress">
|
||||
<referencedMethod name="Twig\Extension\ExtensionInterface::setdateformat" />
|
||||
<file name="app/Providers/TwigProvider.php" />
|
||||
</errorLevel>
|
||||
</UndefinedInterfaceMethod>
|
||||
|
||||
<LessSpecificReturnType errorLevel="info" />
|
||||
|
||||
<!-- level 3 issues - slightly lazy code writing, but provably low false-negatives -->
|
||||
|
||||
<DeprecatedMethod errorLevel="info" />
|
||||
<DeprecatedProperty errorLevel="info" />
|
||||
<DeprecatedClass errorLevel="info" />
|
||||
<DeprecatedConstant errorLevel="info" />
|
||||
<DeprecatedFunction errorLevel="info" />
|
||||
<DeprecatedInterface errorLevel="info" />
|
||||
<DeprecatedTrait errorLevel="info" />
|
||||
|
||||
<InternalMethod errorLevel="info" />
|
||||
<InternalProperty errorLevel="info" />
|
||||
<InternalClass errorLevel="info" />
|
||||
|
||||
<MissingClosureReturnType errorLevel="info" />
|
||||
<MissingReturnType errorLevel="info" />
|
||||
<MissingPropertyType errorLevel="info" />
|
||||
<InvalidDocblock errorLevel="info" />
|
||||
<MisplacedRequiredParam errorLevel="info" />
|
||||
|
||||
<PropertyNotSetInConstructor errorLevel="info" />
|
||||
<MissingConstructor errorLevel="info" />
|
||||
<MissingClosureParamType errorLevel="info" />
|
||||
<MissingParamType errorLevel="info" />
|
||||
|
||||
<RedundantCondition errorLevel="info" />
|
||||
|
||||
<DocblockTypeContradiction errorLevel="info" />
|
||||
<RedundantConditionGivenDocblockType errorLevel="info" />
|
||||
|
||||
<UnresolvableInclude errorLevel="info" />
|
||||
|
||||
<RawObjectIteration errorLevel="info" />
|
||||
|
||||
<InvalidStringClass errorLevel="info" />
|
||||
</issueHandlers>
|
||||
</psalm>
|
41
tailwind.config.js
Normal file
41
tailwind.config.js
Normal file
@@ -0,0 +1,41 @@
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
mono: [
|
||||
'Source Code Pro',
|
||||
'Menlo',
|
||||
'Monaco',
|
||||
'Consolas',
|
||||
'"Liberation Mono"',
|
||||
'"Courier New"',
|
||||
'monospace',
|
||||
],
|
||||
sans: [
|
||||
'Work Sans',
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
'"Segoe UI"',
|
||||
'Roboto',
|
||||
'"Helvetica Neue"',
|
||||
'Arial',
|
||||
'"Noto Sans"',
|
||||
'sans-serif',
|
||||
'"Apple Color Emoji"',
|
||||
'"Segoe UI Emoji"',
|
||||
'"Segoe UI Symbol"',
|
||||
'"Noto Color Emoji"',
|
||||
]
|
||||
},
|
||||
textColor: {
|
||||
github: '#171515',
|
||||
spectrum: '#7B16FF',
|
||||
twitter: '#1DA1F2'
|
||||
}
|
||||
}
|
||||
},
|
||||
variants: {
|
||||
visibility: ['responsive', 'hover', 'group-hover']
|
||||
},
|
||||
plugins: []
|
||||
};
|
19
tests/Bootstrap/AppManangerTest.php
Normal file
19
tests/Bootstrap/AppManangerTest.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Bootstrap;
|
||||
|
||||
use App\Bootstrap\AppManager;
|
||||
use Invoker\CallableResolver;
|
||||
use Slim\App;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AppManangerTest extends TestCase
|
||||
{
|
||||
public function test_it_returns_an_app_instance(): void
|
||||
{
|
||||
$callableResolver = $this->container->get(CallableResolver::class);
|
||||
$app = (new AppManager($this->container, $this->config, $callableResolver))();
|
||||
|
||||
$this->assertInstanceOf(App::class, $app);
|
||||
}
|
||||
}
|
142
tests/Controllers/DirectoryControllerTest.php
Normal file
142
tests/Controllers/DirectoryControllerTest.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Controllers;
|
||||
|
||||
use App\Controllers\DirectoryController;
|
||||
use App\Providers\TwigProvider;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Slim\Psr7\Request;
|
||||
use Slim\Psr7\Response;
|
||||
use Slim\Views\Twig;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DirectoryControllerTest extends TestCase
|
||||
{
|
||||
/** @dataProvider configOptions */
|
||||
public function test_it_returns_a_successful_response(
|
||||
bool $hideAppFiles,
|
||||
bool $hideVcsFiles,
|
||||
bool $displayReadmes
|
||||
): void {
|
||||
$this->config->set('app.hide_app_files', $hideAppFiles);
|
||||
$this->config->set('app.hide_vcs_files', $hideVcsFiles);
|
||||
$this->config->set('app.display_readmes', $displayReadmes);
|
||||
|
||||
$this->container->call(TwigProvider::class);
|
||||
|
||||
$controller = new DirectoryController(
|
||||
$this->container,
|
||||
$this->config,
|
||||
$this->container->get(Twig::class)
|
||||
);
|
||||
|
||||
chdir($this->filePath('.'));
|
||||
$response = $controller(
|
||||
new Finder(),
|
||||
$this->createMock(Request::class),
|
||||
new Response()
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(ResponseInterface::class, $response);
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/** @dataProvider configOptions */
|
||||
public function test_it_returns_a_successful_response_when_listing_a_subdirectory(
|
||||
bool $hideAppFiles,
|
||||
bool $hideVcsFiles,
|
||||
bool $displayReadmes
|
||||
): void {
|
||||
$this->config->set('app.hide_app_files', $hideAppFiles);
|
||||
$this->config->set('app.hide_vcs_files', $hideVcsFiles);
|
||||
$this->config->set('app.display_readmes', $displayReadmes);
|
||||
|
||||
$this->container->call(TwigProvider::class);
|
||||
|
||||
$controller = new DirectoryController(
|
||||
$this->container,
|
||||
$this->config,
|
||||
$this->container->get(Twig::class)
|
||||
);
|
||||
|
||||
chdir($this->filePath('.'));
|
||||
$response = $controller(
|
||||
new Finder(),
|
||||
$this->createMock(Request::class),
|
||||
new Response(),
|
||||
'subdir'
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(ResponseInterface::class, $response);
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function test_it_returns_a_404_error_when_not_found(): void
|
||||
{
|
||||
$this->container->call(TwigProvider::class);
|
||||
|
||||
$controller = new DirectoryController(
|
||||
$this->container,
|
||||
$this->config,
|
||||
$this->container->get(Twig::class)
|
||||
);
|
||||
|
||||
chdir($this->filePath('.'));
|
||||
$response = $controller(
|
||||
new Finder(),
|
||||
$this->createMock(Request::class),
|
||||
new Response(),
|
||||
'404'
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(ResponseInterface::class, $response);
|
||||
$this->assertEquals(404, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function test_it_returns_a_successful_response_for_a_search_request(): void
|
||||
{
|
||||
$this->container->call(TwigProvider::class);
|
||||
|
||||
$controller = new DirectoryController(
|
||||
$this->container,
|
||||
$this->config,
|
||||
$this->container->get(Twig::class)
|
||||
);
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request->method('getQueryParams')->willReturn([
|
||||
'search' => 'charlie'
|
||||
]);
|
||||
|
||||
chdir($this->filePath('.'));
|
||||
$response = $controller(
|
||||
new Finder(),
|
||||
$request,
|
||||
new Response()
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(ResponseInterface::class, $response);
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide config options in the following order:
|
||||
* [ app.hide_app_files, app.hide_vcs_files, app.display_readmes ].
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function configOptions(): array
|
||||
{
|
||||
return [
|
||||
[true, false, false],
|
||||
[true, true, false],
|
||||
[true, false, true],
|
||||
[true, true, true],
|
||||
[false, true, false],
|
||||
[false, true, true],
|
||||
[false, false, true],
|
||||
[false, false, false],
|
||||
];
|
||||
}
|
||||
}
|
42
tests/Controllers/FileInfoControllerTest.php
Normal file
42
tests/Controllers/FileInfoControllerTest.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Controllers;
|
||||
|
||||
use App\Controllers\FileInfoController;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Slim\Psr7\Response;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FileInfoControllerTest extends TestCase
|
||||
{
|
||||
public function test_it_can_return_a_successful_response(): void
|
||||
{
|
||||
$controller = new FileInfoController($this->container, $this->config);
|
||||
|
||||
$response = $controller(new Response(), 'README.md');
|
||||
|
||||
$this->assertInstanceOf(ResponseInterface::class, $response);
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function test_it_can_return_a_not_found_response(): void
|
||||
{
|
||||
$controller = new FileInfoController($this->container, $this->config);
|
||||
|
||||
$response = $controller(new Response(), 'not_a_file.test');
|
||||
|
||||
$this->assertInstanceOf(ResponseInterface::class, $response);
|
||||
$this->assertEquals(404, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function test_it_returns_an_error_when_file_size_is_too_large(): void
|
||||
{
|
||||
$this->config->set('app.max_hash_size', 10);
|
||||
$controller = new FileInfoController($this->container, $this->config);
|
||||
|
||||
$response = $controller(new Response(), 'README.md');
|
||||
|
||||
$this->assertInstanceOf(ResponseInterface::class, $response);
|
||||
$this->assertEquals(500, $response->getStatusCode());
|
||||
}
|
||||
}
|
22
tests/Providers/ConfigProviderTest.php
Normal file
22
tests/Providers/ConfigProviderTest.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Providers;
|
||||
|
||||
use App\Providers\ConfigProvider;
|
||||
use PHLAK\Config\Config;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ConfigProviderTest extends TestCase
|
||||
{
|
||||
public function test_it_can_compose_the_config_component(): void
|
||||
{
|
||||
(new ConfigProvider($this->container))();
|
||||
|
||||
$config = $this->container->get(Config::class);
|
||||
|
||||
$this->assertInstanceOf(Config::class, $config);
|
||||
$this->assertTrue($config->has('app'));
|
||||
$this->assertTrue($config->has('icons'));
|
||||
$this->assertTrue($config->has('view'));
|
||||
}
|
||||
}
|
103
tests/Providers/FinderProviderTest.php
Normal file
103
tests/Providers/FinderProviderTest.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Providers;
|
||||
|
||||
use App\Providers\FinderProvider;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
use Tests\TestCase;
|
||||
|
||||
class FinderProviderTest extends TestCase
|
||||
{
|
||||
public function test_it_can_compose_the_finder_component(): void
|
||||
{
|
||||
(new FinderProvider($this->container, $this->config))();
|
||||
|
||||
$finder = $this->container->get(Finder::class);
|
||||
$finder->in($this->filePath('subdir'))->depth(0);
|
||||
|
||||
$this->assertInstanceOf(Finder::class, $finder);
|
||||
$this->assertEquals([
|
||||
'alpha.scss',
|
||||
'bravo.js',
|
||||
'charlie.bash',
|
||||
'delta.html',
|
||||
'echo.yaml',
|
||||
], $this->getFilesArray($finder));
|
||||
}
|
||||
|
||||
public function test_it_can_sort_by_a_user_provided_closure(): void
|
||||
{
|
||||
$this->config->set('app.sort_order', function (SplFileInfo $file1, SplFileInfo $file2) {
|
||||
return $file1->getSize() <=> $file2->getSize();
|
||||
});
|
||||
|
||||
(new FinderProvider($this->container, $this->config))();
|
||||
|
||||
$finder = $this->container->get(Finder::class);
|
||||
$finder->in($this->filePath('subdir'))->depth(0);
|
||||
|
||||
$this->assertEquals([
|
||||
'alpha.scss',
|
||||
'bravo.js',
|
||||
'echo.yaml',
|
||||
'charlie.bash',
|
||||
'delta.html',
|
||||
], $this->getFilesArray($finder));
|
||||
}
|
||||
|
||||
public function test_it_can_reverse_the_sort_order(): void
|
||||
{
|
||||
$this->config->set('app.reverse_sort', true);
|
||||
|
||||
(new FinderProvider($this->container, $this->config))();
|
||||
|
||||
$finder = $this->container->get(Finder::class);
|
||||
$finder->in($this->filePath('subdir'))->depth(0);
|
||||
|
||||
$this->assertEquals([
|
||||
'echo.yaml',
|
||||
'delta.html',
|
||||
'charlie.bash',
|
||||
'bravo.js',
|
||||
'alpha.scss',
|
||||
], $this->getFilesArray($finder));
|
||||
}
|
||||
|
||||
public function test_it_does_not_return_hidden_files(): void
|
||||
{
|
||||
$this->config->set('app.hidden_files', [
|
||||
'subdir/alpha.scss', 'subdir/charlie.bash', '**/*.yaml'
|
||||
]);
|
||||
|
||||
(new FinderProvider($this->container, $this->config))();
|
||||
|
||||
$finder = $this->container->get(Finder::class);
|
||||
$finder->in($this->filePath('subdir'))->depth(0);
|
||||
|
||||
$this->assertInstanceOf(Finder::class, $finder);
|
||||
$this->assertEquals([
|
||||
'bravo.js',
|
||||
'delta.html',
|
||||
], $this->getFilesArray($finder));
|
||||
}
|
||||
|
||||
public function test_it_throws_a_runtime_exception_with_an_invalid_sort_order(): void
|
||||
{
|
||||
$this->config->set('app.sort_order', 'invalid');
|
||||
|
||||
$this->expectException(RuntimeException::class);
|
||||
|
||||
(new FinderProvider($this->container, $this->config))();
|
||||
}
|
||||
|
||||
protected function getFilesArray(Finder $finder): array
|
||||
{
|
||||
$files = array_map(function (SplFileInfo $file) {
|
||||
return $file->getFilename();
|
||||
}, iterator_to_array($finder));
|
||||
|
||||
return array_values($files);
|
||||
}
|
||||
}
|
42
tests/Providers/TwigProviderTest.php
Normal file
42
tests/Providers/TwigProviderTest.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Providers;
|
||||
|
||||
use App\Providers\TwigProvider;
|
||||
use App\ViewFunctions;
|
||||
use PHLAK\Config\Config;
|
||||
use Slim\Views\Twig;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TwigProviderTest extends TestCase
|
||||
{
|
||||
public function test_it_can_compose_the_view_component(): void
|
||||
{
|
||||
(new TwigProvider($this->container, new Config))();
|
||||
|
||||
$twig = $this->container->get(Twig::class);
|
||||
|
||||
$this->assertInstanceOf(Twig::class, $twig);
|
||||
$this->assertEquals('app/cache/views', $twig->getEnvironment()->getCache());
|
||||
|
||||
$this->assertInstanceOf(
|
||||
ViewFunctions\Asset::class,
|
||||
$twig->getEnvironment()->getFunction('asset')->getCallable()
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(
|
||||
ViewFunctions\Config::class,
|
||||
$twig->getEnvironment()->getFunction('config')->getCallable()
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(
|
||||
ViewFunctions\Icon::class,
|
||||
$twig->getEnvironment()->getFunction('icon')->getCallable()
|
||||
);
|
||||
|
||||
$this->assertInstanceOf(
|
||||
ViewFunctions\SizeForHumans::class,
|
||||
$twig->getEnvironment()->getFunction('sizeForHumans')->getCallable()
|
||||
);
|
||||
}
|
||||
}
|
18
tests/SortMethods/AccessedTest.php
Normal file
18
tests/SortMethods/AccessedTest.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\SortMethods;
|
||||
|
||||
use App\SortMethods\Accessed;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class AccessedTest extends TestCase
|
||||
{
|
||||
public function test_it_can_sort_by_accessed_time(): void
|
||||
{
|
||||
$finder = $this->createMock(Finder::class);
|
||||
$finder->expects($this->once())->method('sortByAccessedTime');
|
||||
|
||||
(new Accessed)($finder);
|
||||
}
|
||||
}
|
18
tests/SortMethods/ChangedTest.php
Normal file
18
tests/SortMethods/ChangedTest.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\SortMethods;
|
||||
|
||||
use App\SortMethods\Changed;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class ChangedTest extends TestCase
|
||||
{
|
||||
public function test_it_can_sort_by_changed_time(): void
|
||||
{
|
||||
$finder = $this->createMock(Finder::class);
|
||||
$finder->expects($this->once())->method('sortByChangedTime');
|
||||
|
||||
(new Changed)($finder);
|
||||
}
|
||||
}
|
18
tests/SortMethods/ModifiedTest.php
Normal file
18
tests/SortMethods/ModifiedTest.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\SortMethods;
|
||||
|
||||
use App\SortMethods\Modified;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class ModifiedTest extends TestCase
|
||||
{
|
||||
public function test_it_can_sort_by_modified_time(): void
|
||||
{
|
||||
$finder = $this->createMock(Finder::class);
|
||||
$finder->expects($this->once())->method('sortByModifiedTime');
|
||||
|
||||
(new Modified)($finder);
|
||||
}
|
||||
}
|
18
tests/SortMethods/NameTest.php
Normal file
18
tests/SortMethods/NameTest.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\SortMethods;
|
||||
|
||||
use App\SortMethods\Name;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class NameTest extends TestCase
|
||||
{
|
||||
public function test_it_can_sort_by_file_name(): void
|
||||
{
|
||||
$finder = $this->createMock(Finder::class);
|
||||
$finder->expects($this->once())->method('sortByName')->with(null);
|
||||
|
||||
(new Name)($finder);
|
||||
}
|
||||
}
|
18
tests/SortMethods/NaturalTest.php
Normal file
18
tests/SortMethods/NaturalTest.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\SortMethods;
|
||||
|
||||
use App\SortMethods\Natural;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class NaturalTest extends TestCase
|
||||
{
|
||||
public function test_it_can_sort_by_natural_file_name(): void
|
||||
{
|
||||
$finder = $this->createMock(Finder::class);
|
||||
$finder->expects($this->once())->method('sortByName')->with(true);
|
||||
|
||||
(new Natural)($finder);
|
||||
}
|
||||
}
|
18
tests/SortMethods/TypeTest.php
Normal file
18
tests/SortMethods/TypeTest.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\SortMethods;
|
||||
|
||||
use App\SortMethods\Type;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
class TypeTest extends TestCase
|
||||
{
|
||||
public function test_it_can_sort_by_file_type(): void
|
||||
{
|
||||
$finder = $this->createMock(Finder::class);
|
||||
$finder->expects($this->once())->method('sortByType');
|
||||
|
||||
(new Type)($finder);
|
||||
}
|
||||
}
|
50
tests/Support/HelpersTest.php
Normal file
50
tests/Support/HelpersTest.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Support;
|
||||
|
||||
use App\Support\Helpers;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class HelpersTest extends TestCase
|
||||
{
|
||||
public function test_it_can_get_an_environment_variable(): void
|
||||
{
|
||||
putenv('TEST_STRING=Test string; please ignore');
|
||||
|
||||
$env = Helpers::env('TEST_STRING');
|
||||
|
||||
$this->assertEquals('Test string; please ignore', $env);
|
||||
}
|
||||
|
||||
public function test_it_can_return_a_default_value(): void
|
||||
{
|
||||
$env = Helpers::env('DEFAULT_TEST', 'Test default; please ignore');
|
||||
|
||||
$this->assertEquals('Test default; please ignore', $env);
|
||||
}
|
||||
|
||||
public function test_it_can_a_retrieve_boolean_value(): void
|
||||
{
|
||||
putenv('TRUE_TEST=true');
|
||||
putenv('FALSE_TEST=false');
|
||||
|
||||
$this->assertTrue(Helpers::env('TRUE_TEST'));
|
||||
$this->assertFalse(Helpers::env('FALSE_TEST'));
|
||||
}
|
||||
|
||||
public function test_it_can_retrieve_a_null_value(): void
|
||||
{
|
||||
putenv('NULL_TEST=null');
|
||||
|
||||
$this->assertNull(Helpers::env('NULL_TEST'));
|
||||
}
|
||||
|
||||
public function test_it_can_be_surrounded_bys_quotation_marks(): void
|
||||
{
|
||||
putenv('QUOTES_TEST="Test charlie; please ignore"');
|
||||
|
||||
$env = Helpers::env('QUOTES_TEST');
|
||||
|
||||
$this->assertEquals('Test charlie; please ignore', $env);
|
||||
}
|
||||
}
|
58
tests/TestCase.php
Normal file
58
tests/TestCase.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use DI\Container;
|
||||
use PHLAK\Config\Config;
|
||||
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
|
||||
|
||||
class TestCase extends PHPUnitTestCase
|
||||
{
|
||||
/** @var Container The test container */
|
||||
protected $container;
|
||||
|
||||
/** @var Config The test config */
|
||||
protected $config;
|
||||
|
||||
/** @var string Path to test files directory */
|
||||
protected $testFilesPath = __DIR__ . '/_files';
|
||||
|
||||
/**
|
||||
* This method is called before each test.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->config = new Config([
|
||||
'app' => [
|
||||
'sort_order' => 'type',
|
||||
'reverse_sort' => false,
|
||||
'hidden_files' => [],
|
||||
'hide_app_files' => true,
|
||||
'hide_vcs_files' => false,
|
||||
'display_readmes' => true,
|
||||
'max_hash_size' => 1000000000,
|
||||
],
|
||||
'view' => [
|
||||
'cache' => false
|
||||
],
|
||||
]);
|
||||
|
||||
$this->container = new Container();
|
||||
$this->container->set('base_path', $this->testFilesPath);
|
||||
$this->container->set(Config::class, $this->config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file path to a test file.
|
||||
*
|
||||
* @param string $filePath
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function filePath(string $filePath): string
|
||||
{
|
||||
return realpath($this->testFilesPath . '/' . $filePath);
|
||||
}
|
||||
}
|
29
tests/ViewFunctions/AssetTest.php
Normal file
29
tests/ViewFunctions/AssetTest.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\ViewFunctions;
|
||||
|
||||
use App\ViewFunctions\Asset;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AssetTest extends TestCase
|
||||
{
|
||||
public function test_it_can_return_an_asset_path(): void
|
||||
{
|
||||
$asset = new Asset($this->container, $this->config);
|
||||
|
||||
$this->assertEquals('/app/dist/test.css', $asset('test.css'));
|
||||
$this->assertEquals(
|
||||
'/app/dist/app.css?id=417c7a9bc03852aafb27',
|
||||
$asset('app.css')
|
||||
);
|
||||
}
|
||||
|
||||
public function test_it_can_return_an_asset_path_without_a_mix_manifest(): void
|
||||
{
|
||||
$this->container->set('base_path', $this->filePath('subdir'));
|
||||
$asset = new Asset($this->container, $this->config);
|
||||
|
||||
$this->assertEquals('/app/dist/test.css', $asset('test.css'));
|
||||
$this->assertEquals('/app/dist/app.css', $asset('app.css'));
|
||||
}
|
||||
}
|
27
tests/ViewFunctions/BreadcrumbsTest.php
Normal file
27
tests/ViewFunctions/BreadcrumbsTest.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\ViewFunctions;
|
||||
|
||||
use App\ViewFunctions\Breadcrumbs;
|
||||
use Tests\TestCase;
|
||||
|
||||
class BreadcrumbsTest extends TestCase
|
||||
{
|
||||
public function test_it_can_parse_breadcrumbs_from_the_path(): void
|
||||
{
|
||||
$breadcrumbs = new Breadcrumbs($this->container, $this->config);
|
||||
|
||||
$this->assertEquals([
|
||||
'foo' => '/foo',
|
||||
'bar' => '/foo/bar',
|
||||
'baz' => '/foo/bar/baz',
|
||||
], $breadcrumbs('foo/bar/baz'));
|
||||
}
|
||||
|
||||
public function test_it_can_parse_breadcrumbs_for_dot_path(): void
|
||||
{
|
||||
$breadcrumbs = new Breadcrumbs($this->container, $this->config);
|
||||
|
||||
$this->assertEquals([], $breadcrumbs('.'));
|
||||
}
|
||||
}
|
40
tests/ViewFunctions/ConfigTest.php
Normal file
40
tests/ViewFunctions/ConfigTest.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\ViewFunctions;
|
||||
|
||||
use App\ViewFunctions\Config;
|
||||
use PHLAK\Config\Config as AppConfig;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ConfigTest extends TestCase
|
||||
{
|
||||
/** @var AppConfig Application config */
|
||||
protected $config;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->config = new AppConfig([
|
||||
'foo' => false,
|
||||
'bar' => 'Red herring',
|
||||
'view' => [
|
||||
'foo' => 'Test value; please ignore'
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_it_can_retrieve_a_config_item(): void
|
||||
{
|
||||
$config = new Config($this->container, $this->config);
|
||||
|
||||
$this->assertEquals('Test value; please ignore', $config('foo'));
|
||||
}
|
||||
|
||||
public function test_it_returns_a_default_value(): void
|
||||
{
|
||||
$config = new Config($this->container, $this->config);
|
||||
|
||||
$this->assertEquals('Default value', $config('bar', 'Default value'));
|
||||
}
|
||||
}
|
55
tests/ViewFunctions/IconTest.php
Normal file
55
tests/ViewFunctions/IconTest.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\ViewFunctions;
|
||||
|
||||
use App\ViewFunctions\Icon;
|
||||
use PHLAK\Config\Config;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
use Tests\TestCase;
|
||||
|
||||
class IconTest extends TestCase
|
||||
{
|
||||
/** @var Config Application config */
|
||||
protected $config;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->config = new Config([
|
||||
'php' => false,
|
||||
'icons' => [
|
||||
'php' => 'fab fa-php',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_it_can_return_icon_markup_for_a_file(): void
|
||||
{
|
||||
$icon = new Icon($this->container, $this->config);
|
||||
$file = $this->createMock(SplFileInfo::class);
|
||||
$file->method('isDir')->willReturn(false);
|
||||
$file->method('getExtension')->willReturn('php');
|
||||
|
||||
$this->assertEquals('<i class="fab fa-php fa-fw fa-lg"></i>', $icon($file));
|
||||
}
|
||||
|
||||
public function test_it_can_return_icon_markup_for_a_directory(): void
|
||||
{
|
||||
$icon = new Icon($this->container, $this->config);
|
||||
$file = $this->createMock(SplFileInfo::class);
|
||||
$file->method('isDir')->willReturn(true);
|
||||
|
||||
$this->assertEquals('<i class="fas fa-folder fa-fw fa-lg"></i>', $icon($file));
|
||||
}
|
||||
|
||||
public function test_it_can_return_the_default_icon_markup(): void
|
||||
{
|
||||
$icon = new Icon($this->container, $this->config);
|
||||
$file = $this->createMock(SplFileInfo::class);
|
||||
$file->method('isDir')->willReturn(false);
|
||||
$file->method('getExtension')->willReturn('default');
|
||||
|
||||
$this->assertEquals('<i class="fas fa-file fa-fw fa-lg"></i>', $icon($file));
|
||||
}
|
||||
}
|
19
tests/ViewFunctions/MarkdownTest.php
Normal file
19
tests/ViewFunctions/MarkdownTest.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\ViewFunctions;
|
||||
|
||||
use App\ViewFunctions\Markdown;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MarkdownTest extends TestCase
|
||||
{
|
||||
public function test_it_can_parse_markdown_into_html(): void
|
||||
{
|
||||
$markdown = new Markdown($this->container, $this->config);
|
||||
|
||||
$this->assertEquals(
|
||||
'<p><strong>Test</strong> <code>markdown</code>, <del>please</del> <em>ignore</em></p>',
|
||||
$markdown('**Test** `markdown`, ~~please~~ _ignore_')
|
||||
);
|
||||
}
|
||||
}
|
16
tests/ViewFunctions/ParentTest.php
Normal file
16
tests/ViewFunctions/ParentTest.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\ViewFunctions;
|
||||
|
||||
use App\ViewFunctions\ParentDir;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ParentDirTest extends TestCase
|
||||
{
|
||||
public function test_it_can_get_the_parent_directory_from_a_path(): void
|
||||
{
|
||||
$parentDir = new ParentDir($this->container, $this->config);
|
||||
|
||||
$this->assertEquals('foo/bar', $parentDir('foo/bar/baz'));
|
||||
}
|
||||
}
|
80
tests/ViewFunctions/SizeForHumansTest.php
Normal file
80
tests/ViewFunctions/SizeForHumansTest.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\ViewFunctions;
|
||||
|
||||
use App\ViewFunctions\SizeForHumans;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SizeForHumansTest extends TestCase
|
||||
{
|
||||
public function test_it_can_convert_bytes_to_bytes(): void
|
||||
{
|
||||
$file = $this->createMock(SplFileInfo::class);
|
||||
$file->method('getSize')->willReturn(13);
|
||||
|
||||
$sizeForHumans = new SizeForHumans($this->container, $this->config);
|
||||
|
||||
$this->assertEquals('13.00B', $sizeForHumans($file));
|
||||
}
|
||||
|
||||
public function test_it_can_convert_bytes_to_kibibytes(): void
|
||||
{
|
||||
$file = $this->createMock(SplFileInfo::class);
|
||||
$file->method('getSize')->willReturn(13690);
|
||||
|
||||
$sizeForHumans = new SizeForHumans($this->container, $this->config);
|
||||
|
||||
$this->assertEquals('13.37KB', $sizeForHumans($file));
|
||||
}
|
||||
|
||||
public function test_it_can_convert_bytes_to_mebibytes(): void
|
||||
{
|
||||
$file = $this->createMock(SplFileInfo::class);
|
||||
$file->method('getSize')->willReturn(14019461);
|
||||
|
||||
$sizeForHumans = new SizeForHumans($this->container, $this->config);
|
||||
|
||||
$this->assertEquals('13.37MB', $sizeForHumans($file));
|
||||
}
|
||||
|
||||
public function test_it_can_convert_bytes_to_gibibytes(): void
|
||||
{
|
||||
$file = $this->createMock(SplFileInfo::class);
|
||||
$file->method('getSize')->willReturn(14355900000);
|
||||
|
||||
$sizeForHumans = new SizeForHumans($this->container, $this->config);
|
||||
|
||||
$this->assertEquals('13.37GB', $sizeForHumans($file));
|
||||
}
|
||||
|
||||
public function test_it_can_convert_bytes_to_tebibytes(): void
|
||||
{
|
||||
$file = $this->createMock(SplFileInfo::class);
|
||||
$file->method('getSize')->willReturn(14700500000000);
|
||||
|
||||
$sizeForHumans = new SizeForHumans($this->container, $this->config);
|
||||
|
||||
$this->assertEquals('13.37TB', $sizeForHumans($file));
|
||||
}
|
||||
|
||||
public function test_it_can_convert_bytes_to_pebibytes(): void
|
||||
{
|
||||
$file = $this->createMock(SplFileInfo::class);
|
||||
$file->method('getSize')->willReturn(15053300000000000);
|
||||
|
||||
$sizeForHumans = new SizeForHumans($this->container, $this->config);
|
||||
|
||||
$this->assertEquals('13.37PB', $sizeForHumans($file));
|
||||
}
|
||||
|
||||
public function test_it_can_convert_bytes_to_exbibytes(): void
|
||||
{
|
||||
$file = $this->createMock(SplFileInfo::class);
|
||||
$file->method('getSize')->willReturn(PHP_INT_MAX);
|
||||
|
||||
$sizeForHumans = new SizeForHumans($this->container, $this->config);
|
||||
|
||||
$this->assertEquals('8.00EB', $sizeForHumans($file));
|
||||
}
|
||||
}
|
1
tests/_files/README.md
Normal file
1
tests/_files/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Test README.md; please ignore
|
1
tests/_files/README.txt
Normal file
1
tests/_files/README.txt
Normal file
@@ -0,0 +1 @@
|
||||
Test README.txt; please ignore
|
3
tests/_files/foxtrot.json
Normal file
3
tests/_files/foxtrot.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"contents": "Test file; please ignore"
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user