| Author | Dave Jarvis <email> |
|---|---|
| Date | 2026-02-08 20:34:24 GMT-0800 |
| Commit | 7bc6c6a0d700ecf248dc97229691cb2db2a0eebf |
| Parent | 25ac3e2 |
| private string $mode; | ||
| private int $timestamp; | ||
| + private int $size; | ||
| private bool $isDir; | ||
| - public function __construct(string $name, string $sha, string $mode, int $timestamp = 0) { | ||
| + public function __construct(string $name, string $sha, string $mode, int $timestamp = 0, int $size = 0) { | ||
| $this->name = $name; | ||
| $this->sha = $sha; | ||
| $this->mode = $mode; | ||
| $this->timestamp = $timestamp; | ||
| + $this->size = $size; | ||
| $this->isDir = ($mode === '40000' || $mode === '040000'); | ||
| } | ||
| public function render(FileRenderer $renderer): void { | ||
| - $renderer->render( | ||
| + $renderer->renderFileItem( | ||
| $this->name, | ||
| $this->sha, | ||
| $this->mode, | ||
| + $this->getIconClass(), | ||
| $this->getTimeElapsed(), | ||
| - $this->isDir, | ||
| - fn(string $type) => $this->isType($type), | ||
| - fn(string $cat) => $this->isCategory($cat) | ||
| + $this->isDir ? '' : $this->getFormattedSize() | ||
| ); | ||
| } | ||
| - /** | ||
| - * Checks if the file matches a specific Media Type string. | ||
| - */ | ||
| + private function getIconClass(): string { | ||
| + if ($this->isDir) return 'fa-folder'; | ||
| + | ||
| + return match (true) { | ||
| + $this->isType('application/pdf') => 'fa-file-pdf', | ||
| + $this->isCategory(MediaTypeSniffer::CAT_ARCHIVE) => 'fa-file-archive', | ||
| + $this->isCategory(MediaTypeSniffer::CAT_IMAGE) => 'fa-file-image', | ||
| + $this->isCategory(MediaTypeSniffer::CAT_AUDIO) => 'fa-file-audio', | ||
| + $this->isCategory(MediaTypeSniffer::CAT_VIDEO) => 'fa-file-video', | ||
| + $this->isCategory(MediaTypeSniffer::CAT_TEXT) => 'fa-file-code', | ||
| + default => 'fa-file', | ||
| + }; | ||
| + } | ||
| + | ||
| + private function getFormattedSize(): string { | ||
| + if ($this->size <= 0) return '0 B'; | ||
| + $units = ['B', 'KB', 'MB', 'GB']; | ||
| + $i = (int)floor(log($this->size, 1024)); | ||
| + return round($this->size / pow(1024, $i), 1) . ' ' . $units[$i]; | ||
| + } | ||
| + | ||
| public function isType(string $type): bool { | ||
| - $data = $this->getSniffBuffer(); | ||
| - return str_contains(MediaTypeSniffer::isMediaType($data, $this->name), $type); | ||
| + return str_contains(MediaTypeSniffer::isMediaType($this->getSniffBuffer(), $this->name), $type); | ||
| } | ||
| - /** | ||
| - * Checks if the file belongs to a specific category (image, video, etc). | ||
| - */ | ||
| public function isCategory(string $category): bool { | ||
| - $data = $this->getSniffBuffer(); | ||
| - return MediaTypeSniffer::isCategory($data, $this->name) === $category; | ||
| + return MediaTypeSniffer::isCategory($this->getSniffBuffer(), $this->name) === $category; | ||
| } | ||
| - /** | ||
| - * Reads the first 12 bytes required for signature sniffing. | ||
| - */ | ||
| private function getSniffBuffer(): string { | ||
| - if ($this->isDir || !file_exists($this->name)) { | ||
| - return ''; | ||
| - } | ||
| - | ||
| + if ($this->isDir || !file_exists($this->name)) return ''; | ||
| $handle = @fopen($this->name, 'rb'); | ||
| - if (!$handle) { | ||
| - return ''; | ||
| - } | ||
| - | ||
| + if (!$handle) return ''; | ||
| $read = fread($handle, 12); | ||
| fclose($handle); | ||
| - | ||
| return ($read !== false) ? $read : ''; | ||
| } | ||
| private function getTimeElapsed(): string { | ||
| if (!$this->timestamp) return ''; | ||
| - | ||
| $diff = time() - $this->timestamp; | ||
| if ($diff < 5) return 'just now'; | ||
| - | ||
| $tokens = [ | ||
| - 31536000 => 'year', | ||
| - 2592000 => 'month', | ||
| - 604800 => 'week', | ||
| - 86400 => 'day', | ||
| - 3600 => 'hour', | ||
| - 60 => 'minute', | ||
| - 1 => 'second' | ||
| + 31536000 => 'year', 2592000 => 'month', 604800 => 'week', | ||
| + 86400 => 'day', 3600 => 'hour', 60 => 'minute', 1 => 'second' | ||
| ]; | ||
| - | ||
| foreach ($tokens as $unit => $text) { | ||
| if ($diff < $unit) continue; |
| <?php | ||
| interface FileRenderer { | ||
| - public function render( | ||
| + public function renderFileItem( | ||
| string $name, | ||
| string $sha, | ||
| string $mode, | ||
| + string $iconClass, | ||
| string $time, | ||
| - bool $isDir, | ||
| - callable $isMediaType | ||
| + string $size = '' | ||
| ): void; | ||
| } | ||
| } | ||
| - public function render( | ||
| + public function renderFileItem( | ||
| string $name, | ||
| string $sha, | ||
| string $mode, | ||
| + string $iconClass, | ||
| string $time, | ||
| - bool $isDir, | ||
| - callable $isMediaType | ||
| + string $size = '' | ||
| ): void { | ||
| - $iconClass = $this->getIconClass($isDir, $isMediaType); | ||
| - | ||
| $url = '?repo=' . urlencode($this->repoSafeName) . '&hash=' . $sha; | ||
| echo '<a href="' . $url . '" class="file-item">'; | ||
| echo '<span class="file-mode">' . $mode . '</span>'; | ||
| echo '<span class="file-name">'; | ||
| - | ||
| - // Render the FontAwesome Icon | ||
| echo '<i class="fas ' . $iconClass . '" style="width: 20px; text-align: center; margin-right: 5px; color: #7a828e;"></i>'; | ||
| - | ||
| echo htmlspecialchars($name); | ||
| echo '</span>'; | ||
| + | ||
| + if ($size) { | ||
| + echo '<span class="file-size" style="color: #8b949e; font-size: 0.8em; margin-left: 10px;">' . $size . '</span>'; | ||
| + } | ||
| if ($time) { | ||
| echo '<span class="file-date" style="color: #8b949e; font-size: 0.8em; margin-left: auto;">' . $time . '</span>'; | ||
| } | ||
| echo '</a>'; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Maps media types to FontAwesome 6 icon classes. | ||
| - */ | ||
| - private function getIconClass(bool $isDir, callable $isMediaType): string { | ||
| - if ($isDir) { | ||
| - return 'fa-folder'; | ||
| - } | ||
| - | ||
| - // Explicit Mime Matches | ||
| - if ($isMediaType('application/pdf')) return 'fa-file-pdf'; | ||
| - | ||
| - // Archives | ||
| - $archives = [ | ||
| - 'application/zip', 'application/x-tar', 'application/gzip', | ||
| - 'application/x-bzip2', 'application/vnd.rar', 'application/x-7z-compressed' | ||
| - ]; | ||
| - foreach ($archives as $archive) { | ||
| - if ($isMediaType($archive)) return 'fa-file-archive'; | ||
| - } | ||
| - | ||
| - // Broad Categories (based on prefix) | ||
| - if ($isMediaType('image/')) return 'fa-file-image'; | ||
| - if ($isMediaType('audio/')) return 'fa-file-audio'; | ||
| - if ($isMediaType('video/')) return 'fa-file-video'; | ||
| - | ||
| - // Code / Text | ||
| - if ($isMediaType('text/')) return 'fa-file-code'; | ||
| - | ||
| - // Common Application Types behaving like code/text | ||
| - if ( | ||
| - $isMediaType('javascript') || | ||
| - $isMediaType('json') || | ||
| - $isMediaType('xml') || | ||
| - $isMediaType('php') || | ||
| - $isMediaType('sh') | ||
| - ) { | ||
| - return 'fa-file-code'; | ||
| - } | ||
| - | ||
| - // Default fallback | ||
| - return 'fa-file'; | ||
| } | ||
| } | ||
| // Categories | ||
| - 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_APP = 'application'; | ||
| - private const CAT_BINARY = 'binary'; | ||
| + public const CAT_IMAGE = 'image'; | ||
| + public const CAT_VIDEO = 'video'; | ||
| + public const CAT_AUDIO = 'audio'; | ||
| + public const CAT_TEXT = 'text'; | ||
| + public const CAT_ARCHIVE = 'archive'; | ||
| + public const CAT_APP = 'application'; | ||
| + public const CAT_BINARY = 'binary'; | ||
| private const FORMATS = [ | ||
| /** | ||
| - * Returns [Category, Mime] or null if not found. | ||
| + * Internal helper to resolve category and mime type. | ||
| + * Guaranteed to return a non-empty array. | ||
| */ | ||
| - private static function sniff( $data ): ?array { | ||
| - if( !empty( $data ) ) { | ||
| - $dataLength = strlen( $data ); | ||
| - $maxScan = min( $dataLength, self::BUFFER ); | ||
| - $sourceBytes = []; | ||
| + private static function getTypeInfo( string $data, string $filePath ): array { | ||
| + $info = self::sniff( $data ); | ||
| - for( $i = 0; $i < $maxScan; $i++ ) { | ||
| - $sourceBytes[$i] = ord( $data[$i] ) & 0xFF; | ||
| - } | ||
| + if ( empty( $info ) && !empty( $filePath ) ) { | ||
| + $info = self::getInfoByExtension( $filePath ); | ||
| + } | ||
| - foreach( self::FORMATS as [$category, $pattern, $type] ) { | ||
| - $patternLength = count( $pattern ); | ||
| - if( $patternLength > $dataLength ) continue; | ||
| + return !empty( $info ) ? $info : [self::CAT_BINARY, 'application/octet-stream']; | ||
| + } | ||
| - $matches = true; | ||
| - for( $i = 0; $i < $patternLength; $i++ ) { | ||
| - if( $pattern[$i] !== self::ANY && $pattern[$i] !== $sourceBytes[$i] ) { | ||
| - $matches = false; | ||
| - break; | ||
| - } | ||
| - } | ||
| + private static function sniff( string $data ): array { | ||
| + if( empty( $data ) ) return []; | ||
| - if( $matches ) { | ||
| - return [$category, $type]; | ||
| + $dataLength = strlen( $data ); | ||
| + $maxScan = min( $dataLength, self::BUFFER ); | ||
| + $sourceBytes = []; | ||
| + | ||
| + for( $i = 0; $i < $maxScan; $i++ ) { | ||
| + $sourceBytes[$i] = ord( $data[$i] ) & 0xFF; | ||
| + } | ||
| + | ||
| + foreach( self::FORMATS as [$category, $pattern, $type] ) { | ||
| + $patternLength = count( $pattern ); | ||
| + | ||
| + if( $patternLength > $dataLength ) continue; | ||
| + | ||
| + $matches = true; | ||
| + | ||
| + for( $i = 0; $i < $patternLength; $i++ ) { | ||
| + if( $pattern[$i] !== self::ANY && $pattern[$i] !== $sourceBytes[$i] ) { | ||
| + $matches = false; | ||
| + break; | ||
| } | ||
| } | ||
| + | ||
| + if( $matches ) return [$category, $type]; | ||
| } | ||
| - return null; | ||
| + | ||
| + return []; | ||
| } | ||
| - private static function getInfoByExtension( $filePath ): array { | ||
| + private static function getInfoByExtension( string $filePath ): array { | ||
| $extension = strtolower( pathinfo( $filePath, PATHINFO_EXTENSION ) ); | ||
| return self::EXTENSION_MAP[$extension] ?? [self::CAT_BINARY, 'application/octet-stream']; | ||
| } | ||
| - | ||
| - public static function isMediaType( $data, $filePath = '' ): string { | ||
| - $info = self::sniff( $data ); | ||
| - | ||
| - if ( $info === null && !empty( $filePath ) ) { | ||
| - $info = self::getInfoByExtension( $filePath ); | ||
| - } | ||
| - return $info ? $info[1] : 'application/octet-stream'; | ||
| + public static function isMediaType( string $data, string $filePath = '' ): string { | ||
| + return self::getTypeInfo( $data, $filePath )[1]; | ||
| } | ||
| - | ||
| - public static function isCategory( $data, $filePath = '' ): string { | ||
| - $info = self::sniff( $data ); | ||
| - if ( $info === null && !empty( $filePath ) ) { | ||
| - $info = self::getInfoByExtension( $filePath ); | ||
| - } | ||
| + public static function isCategory( string $data, string $filePath = '' ): string { | ||
| + return self::getTypeInfo( $data, $filePath )[0]; | ||
| + } | ||
| - return $info ? $info[0] : self::CAT_BINARY; | ||
| + public static function isBinary( string $data, string $filePath = '' ): bool { | ||
| + return self::isCategory( $data, $filePath ) !== self::CAT_TEXT; | ||
| } | ||
| } | ||
| Delta | 92 lines added, 136 lines removed, 44-line decrease |
|---|