<?php require_once __DIR__ . '/../render/FileRenderer.php'; class File { private const CAT_IMAGE = 'image'; private const CAT_VIDEO = 'video'; private const CAT_AUDIO = 'audio'; private const CAT_TEXT = 'text'; private const CAT_ARCHIVE = 'archive'; private const CAT_BINARY = 'binary'; private const ICON_FOLDER = 'fa-folder'; private const ICON_PDF = 'fa-file-pdf'; private const ICON_ARCHIVE = 'fa-file-archive'; private const ICON_IMAGE = 'fa-file-image'; private const ICON_AUDIO = 'fa-file-audio'; private const ICON_VIDEO = 'fa-file-video'; private const ICON_CODE = 'fa-file-code'; private const ICON_FILE = 'fa-file'; private const MODE_DIR = '40000'; private const MODE_DIR_LONG = '040000'; private const MEDIA_EMPTY = 'application/x-empty'; private const MEDIA_OCTET = 'application/octet-stream'; private const MEDIA_PDF = 'application/pdf'; private const MEDIA_TEXT = 'text/'; private const MEDIA_SVG = 'image/svg'; private const MEDIA_APP_TEXT = [ 'application/javascript', 'application/json', 'application/xml', 'application/x-httpd-php', 'application/x-sh' ]; private const ARCHIVE_EXT = [ 'zip', 'tar', 'gz', '7z', 'rar', 'jar', 'lha', 'bz', 'tgz', 'cab', 'iso', 'dmg', 'xz', 'z', 'ar', 'war', 'ear', 'pak', 'hqx', 'arj', 'zoo', 'rpm', 'deb', 'apk' ]; private string $name; private string $sha; private string $mode; private int $timestamp; private int $size; private bool $isDir; private string $mediaType; private string $category; private bool $binary; private static ?finfo $finfo = null; public function __construct( string $name, string $sha, string $mode, int $timestamp, int $size, string $contents = '' ) { $this->name = $name; $this->sha = $sha; $this->mode = $mode; $this->timestamp = $timestamp; $this->size = $size; $this->isDir = $mode === self::MODE_DIR || $mode === self::MODE_DIR_LONG; $buffer = $this->isDir ? '' : $contents; $this->mediaType = $this->detectMediaType( $buffer ); $this->category = $this->detectCategory( $name ); $this->binary = $this->detectBinary(); } public function isEmpty(): bool { return $this->size === 0; } public function compare( File $other ): int { return $this->isDir !== $other->isDir ? ($this->isDir ? -1 : 1) : strcasecmp( $this->name, $other->name ); } public function renderListEntry( FileRenderer $renderer ): void { $renderer->renderListEntry( $this->name, $this->sha, $this->mode, $this->resolveIcon(), $this->timestamp, $this->size ); } public function emitRawHeaders(): void { header( "Content-Type: " . $this->mediaType ); header( "Content-Length: " . $this->size ); header( "Content-Disposition: attachment; filename=\"" . addslashes( basename( $this->name ) ) . "\"" ); } public function renderMedia( FileRenderer $renderer, string $url ): bool { return $renderer->renderMedia( $this, $url, $this->mediaType ); } public function renderSize( FileRenderer $renderer ): void { $renderer->renderSize( $this->size ); } public function highlight( FileRenderer $renderer, string $content ): string { return $renderer->highlight( $this->name, $content, $this->mediaType ); } public function isDir(): bool { return $this->isDir; } public function isImage(): bool { return $this->category === self::CAT_IMAGE; } public function isVideo(): bool { return $this->category === self::CAT_VIDEO; } public function isAudio(): bool { return $this->category === self::CAT_AUDIO; } public function isText(): bool { return $this->category === self::CAT_TEXT; } public function isBinary(): bool { return $this->binary; } public function isName( string $name ): bool { return $this->name === $name; } private function resolveIcon(): string { return $this->isDir ? self::ICON_FOLDER : (str_contains( $this->mediaType, self::MEDIA_PDF ) ? self::ICON_PDF : match( $this->category ) { self::CAT_ARCHIVE => self::ICON_ARCHIVE, self::CAT_IMAGE => self::ICON_IMAGE, self::CAT_AUDIO => self::ICON_AUDIO, self::CAT_VIDEO => self::ICON_VIDEO, self::CAT_TEXT => self::ICON_CODE, default => self::ICON_FILE, }); } private static function fileinfo(): finfo { return self::$finfo ??= new finfo( FILEINFO_MIME_TYPE ); } private function detectMediaType( string $buffer ): string { return $buffer === '' ? self::MEDIA_EMPTY : (self::fileinfo()->buffer( substr( $buffer, 0, 128 ) ) ?: self::MEDIA_OCTET); } private function detectCategory( string $filename ): string { $main = explode( '/', $this->mediaType )[0]; $main = $this->isArchive( $filename ) || str_contains( $this->mediaType, 'compressed' ) ? self::CAT_ARCHIVE : $main; $main = $main !== self::CAT_ARCHIVE && $this->isMediaTypeText() ? 'text' : $main; return match( $main ) { 'image' => self::CAT_IMAGE, 'video' => self::CAT_VIDEO, 'audio' => self::CAT_AUDIO, 'text' => self::CAT_TEXT, self::CAT_ARCHIVE => self::CAT_ARCHIVE, default => self::CAT_BINARY, }; } private function detectBinary(): bool { return $this->mediaType !== self::MEDIA_EMPTY && !$this->isMediaTypeText() && !str_contains( $this->mediaType, self::MEDIA_SVG ); } private function isMediaTypeText(): bool { return str_starts_with( $this->mediaType, self::MEDIA_TEXT ) || in_array( $this->mediaType, self::MEDIA_APP_TEXT, true ); } private function isArchive( string $filename ): bool { return in_array( strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) ), self::ARCHIVE_EXT, true ); } }