Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/treetrek.git
<?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;

  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 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 function detectMediaType( string $buffer ): string {
    return $buffer === ''
      ? self::MEDIA_EMPTY
      : ((new finfo( FILEINFO_MIME_TYPE ))
          ->buffer( substr( $buffer, 0, 256 ) )
        ?: 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
    );
  }
}