<commit_before><?php namespace App;

use Illuminate\Database\Eloquent\Model;
use Intervention\Image\ImageManager;
use Symfony\Component\HttpFoundation\File\File as SymfonyFile;
use Symfony\Component\HttpFoundation\File\UploadedFile;

use Cache;
use Exception;
use File;
use Input;
use Settings;
use Sleuth;
use Storage;
use Request;

class FileStorage extends Model {

	/**
	 * The database table used by the model.
	 *
	 * @var string
	 */
	protected $table = 'files';

	/**
	 * The database primary key.
	 *
	 * @var string
	 */
	protected $primaryKey = 'file_id';

	/**
	 * The attributes that are mass assignable.
	 *
	 * @var array
	 */
	protected $fillable = [
		'hash',
		'banned',
		'filesize',
		'file_width',
		'file_height',
		'mime',
		'meta',
		'first_uploaded_at',
		'last_uploaded_at',
		'upload_count',
		'has_thumbnail',
		'thumbnail_width',
		'thumbnail_height'
	];

	/**
	 * Determines if Laravel should set created_at and updated_at timestamps.
	 *
	 * @var array
	 */
	public $timestamps = false;


	/**
	 * The \App\FileAttachment relationship.
	 * Represents a post -> storage relationship.
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\HasMany
	 */
	public function attachments()
	{
		return $this->hasMany('\App\FileAttachment', 'file_id');
	}

	/**
	 * The \App\BoardAsset relationship.
	 * Used for multiple custom facets of a board.
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\HasMany
	 */
	public function assets()
	{
		return $this->hasMany('\App\BoardAsset', 'file_id');
	}

	/**
	 * The \App\Posts relationship.
	 * Uses the attachments() relationship to find posts where this file is attached..
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
	 */
	public function posts()
	{
		return $this->belongsToMany("\App\Post", 'file_attachments', 'file_id', 'post_id')->withPivot('filename', 'position');
	}


	/**
	 * Will trigger a file deletion if the storage item is not used anywhere.
	 *
	 * @return boolean
	 */
	public function challengeExistence()
	{
		$count = $this->assets->count() + $this->attachments->count();

		if ($count === 0)
		{
			$this->deleteFile();
			return false;
		}

		return true;
	}

	public static function checkUploadExists(UploadedFile $file, Board $board, Post $thread = null)
	{
		return static::checkHashExists(md5((string) File::get($file)), $board, $thread);
	}

	public static function checkHashExists($hash, Board $board, Post $thread = null)
	{
		$query = Post::where('board_uri', $board->board_uri);

		if (!is_null($thread))
		{
			$query = $query->whereInThread($thread);
		}

		return $query->whereHas('attachments', function($query) use ($hash) {
			$query->whereHash($hash);
		})->first();
	}

	/**
	 * Creates a new FileAttachment for a post using a direct upload.
	 *
	 * @param  UploadedFile  $file
	 * @param  Post  $post
	 * @return FileAttachment
	 */
	public static function createAttachmentFromUpload(UploadedFile $file, Post $post, $autosave = true)
	{
		$storage     = static::storeUpload($file);

		$uploadName  = urlencode($file->getClientOriginalName());
		$uploadExt   = pathinfo($uploadName, PATHINFO_EXTENSION);

		$fileName    = basename($uploadName, "." . $uploadExt);
		$fileExt     = $storage->guessExtension();

		$attachment  = new FileAttachment();
		$attachment->post_id    = $post->post_id;
		$attachment->file_id    = $storage->file_id;
		$attachment->filename   = urlencode("{$fileName}.{$fileExt}");
		$attachment->is_spoiler = !!Input::get('spoilers');

		if ($autosave)
		{
			$attachment->save();

			$storage->upload_count++;
			$storage->save();
		}

		return $attachment;
	}

	/**
	 * Creates a new FileAttachment for a post using a hash.
	 *
	 * @param  Post  $post
	 * @param  string  $filename
	 * @param  boolean  $spoiler
	 * @return FileAttachment
	 */
	public function createAttachmentWithThis(Post $post, $filename, $spoiler = false, $autosave = true)
	{
		$fileName    = pathinfo($filename, PATHINFO_FILENAME);
		$fileExt     = $this->guessExtension();

		$attachment  = new FileAttachment();
		$attachment->post_id    = $post->post_id;
		$attachment->file_id    = $this->file_id;
		$attachment->filename   = urlencode("{$fileName}.{$fileExt}");
		$attachment->is_spoiler = !!$spoiler;

		if ($autosave)
		{
			$attachment->save();

			$this->upload_count++;
			$this->save();
		}

		return $attachment;
	}

	/**
	 * Removes the associated file for this storage.
	 *
	 * @return boolean  Success. Will return FALSE if the file was already gone.
	 */
	public function deleteFile()
	{
		return unlink($this->getFullPath()) && unlink($this->getFullPathThumb());
	}

	/**
	 * Returns the storage's file as a filesystem.
	 *
	 * @return \Illuminate\Filesystem\Filesystem
	 */
	public function getAsFile()
	{
		return new File($this->getFullPath());
	}

	/**
	 * Returns the storage's thumbnail as a filesystem.
	 *
	 * @return \Illuminate\Filesystem\Filesystem
	 */
	public function getAsFileThumb()
	{
		return new File($this->getFullPathThumb());
	}

	/**
	 * Returns the attachment's base filename.
	 *
	 * @return string
	 */
	public function getBaseFileName()
	{
		$pathinfo  = pathinfo($this->pivot->filename);
		return $pathinfo['filename'];
	}

	/**
	 * Returns the storage directory, minus the file name.
	 *
	 * @return string
	 */
	public function getDirectory()
	{
		$prefix = $this->getHashPrefix($this->hash);

		return "attachments/full/{$prefix}";
	}

	/**
	 * Returns the thumbnail's storage directory, minus the file name.
	 *
	 * @return string
	 */
	public function getDirectoryThumb()
	{
		$prefix = $this->getHashPrefix($this->hash);

		return "attachments/thumb/{$prefix}";
	}

	/**
	 * Supplies a download name.
	 *
	 * @return string
	 */
	public function getDownloadName()
	{
		return "{$this->getFileName()}.{$this->guessExtension()}";
	}

	/**
	 * Supplies a clean URL for downloading an attachment on a board.
	 *
	 * @param  App\Board  $board
	 * @return string
	 */
	public function getDownloadURL(Board $board)
	{
		//return url("/{$board->board_uri}/file/{$this->pivot->attachment_id}/{$this->getDownloadName()}");

		$params = [
			'attachment' => $this->pivot->attachment_id,
			'filename'   => $this->getDownloadName(),
		];

		if (!env('APP_MEDIA_URL', false))
		{
			$params['board'] = $board;
		}

		return route('static.file.attachment', $params, false);
	}

	/**
	 * Returns the attachment's extension.
	 *
	 * @return string
	 */
	public function getExtension()
	{
		$pathinfo  = pathinfo($this->pivot->filename);
		return $pathinfo['extension'];
	}

	/**
	 * Returns the dimensions of the thumbnail, if possible.
	 *
	 * @return string|null
	 */
	public function getFileDimensions()
	{
		if ($this->has_thumbnail)
		{
			return "{$this->file_width}x{$this->file_height}";
		}

		return null;
	}

	/**
	 * Determines and returns the "xxx" of "/url/xxx.ext" for URLs.
	 *
	 * @param  string|null  $format  Optional. The token syntax for the filename. Defaults to site setting.
	 * @return string
	 */
	public function getFileName($nameFormat = null)
	{
		if (is_null($nameFormat))
		{
			// Build a thumbnail using the admin settings.
			$nameFormat = Settings::get('attachmentName');
		}

		$first_uploade_at = new \Carbon\Carbon($this->first_uploaded_at);

		$bits['t'] = $first_uploade_at->timestamp;
		$bits['i'] = 0;
		$bits['n'] = $bits['t'];

		if (isset($this->pivot))
		{
			if (isset($this->pivot->position))
			{
				$bits['i'] = $this->pivot->position;
			}

			if (isset($this->pivot->filename))
			{
				$bits['n'] = $this->getBaseFileName();
			}
		}

		$attachmentName = $nameFormat;

		foreach ($bits as $bitKey => $bitVal)
		{
			$attachmentName = str_replace("%{$bitKey}", $bitVal, $attachmentName);
		}

		return $attachmentName;
	}

	/**
	 * Returns the full internal file path.
	 *
	 * @return string
	 */
	public function getFullPath()
	{
		$storagePath = Storage::disk('local')->getDriver()->getAdapter()->getPathPrefix();
		return "{$storagePath}{$this->getPath()}";
	}

	/**
	 * Returns the full internal file path for the thumbnail.
	 *
	 * @return string
	 */
	public function getFullPathThumb()
	{
		$storagePath = Storage::disk('local')->getDriver()->getAdapter()->getPathPrefix();
		return "{$storagePath}{$this->getPathThumb()}";
	}

	/**
	 * Fetch an instance of static using the checksum.
	 *
	 * @param  $hash  Checksum
	 * @return static|null
	 */
	public static function getHash($hash)
	{
		return static::whereHash($hash)->get()->first();
	}

	/**
	 * Returns the skip file directoy prefix
	 *
	 * @param  $hash  Checksum
	 * @return static  Like "a/a/a/a"
	 */
	public static function getHashPrefix($hash)
	{
		return implode(str_split(substr($hash, 0, 4)), "/");
	}

	/**
	 * Converts the byte size to a human-readable filesize.
	 *
	 * @author <NAME>
	 * @param  int  $decimals
	 * @return string
	 */
	public function getHumanFilesize($decimals = 2)
	{
		$bytes  = $this->filesize;
		$size   = array('B', 'kiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB');
		$factor = floor((strlen($bytes) - 1) / 3);

		return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . " " . @$size[$factor];
	}

	/**
	 * Returns a URL part based on available information.
	 *
	 * @return string|int
	 */
	public function getIdentifier()
	{
		if (isset($this->pivot))
		{
			return $this->pivot->attachment_id;
		}

		return $this->hash;
	}

	/**
	 * Returns the relative internal file path.
	 *
	 * @return string
	 */
	public function getPath()
	{
		return $this->getDirectory() . "/" . $this->hash;
	}

	/**
	 * Returns the full internal file path for the thumbnail.
	 *
	 * @return string
	 */
	public function getPathThumb()
	{
		return $this->getDirectoryThumb() . "/" . $this->hash;
	}

	/**
	 * Returns a removal URL.
	 *
	 * @param  \App\Board  $board
	 * @return string
	 */
	public function getRemoveURL(Board $board)
	{
		return url("{$board->board_uri}/file/remove/{$this->pivot->attachment_id}", [], false);
	}

	/**
	 * Truncates the middle of a filename to show extension.
	 *
	 * @return string  Filename.
	 */
	public function getShortFilename()
	{
		if (isset($this->pivot) && isset($this->pivot->filename))
		{
			$filename = urldecode($this->pivot->filename);

			if (mb_strlen($filename) <= 20)
			{
				return $filename;
			}

			$ext  = pathinfo($filename, PATHINFO_EXTENSION);
			$name = pathinfo($filename, PATHINFO_FILENAME);
			$name = mb_substr($name, 0, 15);

			return "{$name}... .{$ext}";
		}

		return $this->getFileName();
	}

	/**
	 * Returns a spoiler URL.
	 *
	 * @param  \App\Board  $board
	 * @return string
	 */
	public function getSpoilerURL(Board $board)
	{
		return url("{$board->board_uri}/file/spoiler/{$this->pivot->attachment_id}", [], false);
	}

	/**
	 * Returns a string containing class names.
	 *
	 * @return string
	 */
	public function getThumbnailClasses()
	{
		$ext   = $this->guessExtension();
		$type  = "other";
		$stock = true;
		$spoil = $this->isSpoiler();

		if ($this->isImageVector())
		{
			$stock = false;
			$type  = "img";
		}
		else if ($this->isImage())
		{
			if ($this->hasThumb())
			{
				$stock = false;
				$type  = "img";
			}
		}
		else if ($this->isVideo())
		{
			if ($this->hasThumb())
			{
				$stock = false;
				$type  = "video";
			}
		}
		else if ($this->isAudio())
		{
			$stock = false;
			$type  = "audio";
		}

		$classes = [];
		$classes['type']  = "attachment-type-{$type}";
		$classes['ext']   = "attachent-ext-{$ext}";
		$classes['stock'] = $stock ? "thumbnail-stock" : "thumbnail-content";
		$classes['spoil'] = $spoil ? "thumbnail-spoiler" : "thumbnail-not-spoiler";
		$classHTML = implode(" ", $classes);

		return $classHTML;
	}

	/**
	 * Returns an XML valid attachment HTML string that handles missing thumbnail URLs.
	 *
	 * @param  \App\Board  $board  The board this thumbnail will belong to.
	 * @param  int  $maxWidth  Optional. Maximum width constraint. Defaults null.
	 * @return string  as HTML
	 */
	public function getThumbnailHTML(Board $board, $maxDimension = null)
	{
		$ext     = $this->guessExtension();
		$mime    = $this->mime;
		$url     = asset("static/img/filetypes/{$ext}.svg");
		$spoil   = $this->isSpoiler();
		$deleted = $this->isDeleted();

		if ($deleted)
		{
			$url = $board->getAssetUrl('file_deleted');
		}
		else if ($spoil)
		{
			$url = $board->getAssetUrl('file_spoiler');
		}
		else if ($this->isImageVector())
		{
			$url = $this->getDownloadURL($board);
		}
		else if ($this->isAudio() || $this->isImage() || $this->isVideo())
		{
			if ($this->hasThumb())
			{
				$url = $this->getThumbnailURL($board);
			}
			else if ($this->isAudio())
			{
				$url = asset("static/img/assets/audio.gif");
			}
		}

		$classHTML = $this->getThumbnailClasses();


		// Measure dimensions.
		$height    = "auto";
		$width     = "auto";
		$maxWidth  = "none";
		$maxHeight = "none";
		$minWidth  = "none";
		$minHeight = "none";
		$oHeight   = $this->thumbnail_height;
		$oWidth    = $this->thumbnail_width;

		if ($this->has_thumbnail && !$this->isSpoiler() && !$this->isDeleted())
		{
			$height = $oHeight . "px";
			$width  = $this->thumbnail_width . "px";

			if (is_integer($maxDimension) && ($oWidth > $maxDimension || $oHeight > $maxDimension))
			{
				if ($oWidth > $oHeight)
				{
					$height = "auto";
					$width  = $maxDimension . "px";
				}
				else if ($oWidth < $oHeight)
				{
					$height = $maxDimension . "px";
					$width  = "auto";
				}
				else
				{
					$height = $maxDimension;
					$width  = $maxDimension;
				}
			}

			$minWidth = $width;
			$minHeight = $height;
		}
		else
		{
			$maxWidth  = Settings::get('attachmentThumbnailSize') . "px";
			$maxHeight = $maxWidth;
			$width     = $maxWidth;
			$height    = "auto";

			if (is_integer($maxDimension))
			{
				$maxWidth = "{$maxDimension}px";
				$maxHeight = "{$maxDimension}px";
			}

			if ($this->isSpoiler() || $this->isDeleted())
			{
				$minHeight = "none";
				$minWidth = "none";
				$width = $maxWidth;
			}
		}

		return "<div class=\"attachment-wrapper\" style=\"height: {$height}; width: {$width};\">" .
			"<img class=\"attachment-img {$classHTML}\" src=\"{$url}\" data-mime=\"{$mime}\" style=\"height: {$height}; width: {$width};\"/>" .
		"</div>";
	}

	/**
	 * Supplies a clean thumbnail URL for embedding an attachment on a board.
	 *
	 * @param  \App\Board  $board
	 * @return string
	 */
	public function getThumbnailURL(Board $board)
	{
		$ext = $this->guessExtension();

		if ($this->isSpoiler())
		{
			return $board->getSpoilerUrl();
		}

		if ($this->isImage())
		{
			$ext = Settings::get('attachmentThumbnailJpeg') ? "jpg" : "png";
		}
		else if ($this->isVideo())
		{
			$ext = "jpg";
		}
		else if ($this->isAudio())
		{
			if (!$this->hasThumb())
			{
				return $board->getAudioArtURL();
			}

			$ext = "png";
		}
		else if ($this->isImageVector())
		{
			// With the SVG filetype, we do not generate a thumbnail, so just serve the actual SVG.
			return $this->getDownloadURL($board);
		}

		$params = [
			'attachment' => $this->getIdentifier(),
			'filename'   => $this->getDownloadName(),
		];

		if (!env('APP_MEDIA_URL', false))
		{<commit_msg>Fixed extensions on the thumbnail URLs.<commit_after><?php namespace App;

use Illuminate\Database\Eloquent\Model;
use Intervention\Image\ImageManager;
use Symfony\Component\HttpFoundation\File\File as SymfonyFile;
use Symfony\Component\HttpFoundation\File\UploadedFile;

use Cache;
use Exception;
use File;
use Input;
use Settings;
use Sleuth;
use Storage;
use Request;

class FileStorage extends Model {

	/**
	 * The database table used by the model.
	 *
	 * @var string
	 */
	protected $table = 'files';

	/**
	 * The database primary key.
	 *
	 * @var string
	 */
	protected $primaryKey = 'file_id';

	/**
	 * The attributes that are mass assignable.
	 *
	 * @var array
	 */
	protected $fillable = [
		'hash',
		'banned',
		'filesize',
		'file_width',
		'file_height',
		'mime',
		'meta',
		'first_uploaded_at',
		'last_uploaded_at',
		'upload_count',
		'has_thumbnail',
		'thumbnail_width',
		'thumbnail_height'
	];

	/**
	 * Determines if Laravel should set created_at and updated_at timestamps.
	 *
	 * @var array
	 */
	public $timestamps = false;


	/**
	 * The \App\FileAttachment relationship.
	 * Represents a post -> storage relationship.
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\HasMany
	 */
	public function attachments()
	{
		return $this->hasMany('\App\FileAttachment', 'file_id');
	}

	/**
	 * The \App\BoardAsset relationship.
	 * Used for multiple custom facets of a board.
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\HasMany
	 */
	public function assets()
	{
		return $this->hasMany('\App\BoardAsset', 'file_id');
	}

	/**
	 * The \App\Posts relationship.
	 * Uses the attachments() relationship to find posts where this file is attached..
	 *
	 * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
	 */
	public function posts()
	{
		return $this->belongsToMany("\App\Post", 'file_attachments', 'file_id', 'post_id')->withPivot('filename', 'position');
	}


	/**
	 * Will trigger a file deletion if the storage item is not used anywhere.
	 *
	 * @return boolean
	 */
	public function challengeExistence()
	{
		$count = $this->assets->count() + $this->attachments->count();

		if ($count === 0)
		{
			$this->deleteFile();
			return false;
		}

		return true;
	}

	public static function checkUploadExists(UploadedFile $file, Board $board, Post $thread = null)
	{
		return static::checkHashExists(md5((string) File::get($file)), $board, $thread);
	}

	public static function checkHashExists($hash, Board $board, Post $thread = null)
	{
		$query = Post::where('board_uri', $board->board_uri);

		if (!is_null($thread))
		{
			$query = $query->whereInThread($thread);
		}

		return $query->whereHas('attachments', function($query) use ($hash) {
			$query->whereHash($hash);
		})->first();
	}

	/**
	 * Creates a new FileAttachment for a post using a direct upload.
	 *
	 * @param  UploadedFile  $file
	 * @param  Post  $post
	 * @return FileAttachment
	 */
	public static function createAttachmentFromUpload(UploadedFile $file, Post $post, $autosave = true)
	{
		$storage     = static::storeUpload($file);

		$uploadName  = urlencode($file->getClientOriginalName());
		$uploadExt   = pathinfo($uploadName, PATHINFO_EXTENSION);

		$fileName    = basename($uploadName, "." . $uploadExt);
		$fileExt     = $storage->guessExtension();

		$attachment  = new FileAttachment();
		$attachment->post_id    = $post->post_id;
		$attachment->file_id    = $storage->file_id;
		$attachment->filename   = urlencode("{$fileName}.{$fileExt}");
		$attachment->is_spoiler = !!Input::get('spoilers');

		if ($autosave)
		{
			$attachment->save();

			$storage->upload_count++;
			$storage->save();
		}

		return $attachment;
	}

	/**
	 * Creates a new FileAttachment for a post using a hash.
	 *
	 * @param  Post  $post
	 * @param  string  $filename
	 * @param  boolean  $spoiler
	 * @return FileAttachment
	 */
	public function createAttachmentWithThis(Post $post, $filename, $spoiler = false, $autosave = true)
	{
		$fileName    = pathinfo($filename, PATHINFO_FILENAME);
		$fileExt     = $this->guessExtension();

		$attachment  = new FileAttachment();
		$attachment->post_id    = $post->post_id;
		$attachment->file_id    = $this->file_id;
		$attachment->filename   = urlencode("{$fileName}.{$fileExt}");
		$attachment->is_spoiler = !!$spoiler;

		if ($autosave)
		{
			$attachment->save();

			$this->upload_count++;
			$this->save();
		}

		return $attachment;
	}

	/**
	 * Removes the associated file for this storage.
	 *
	 * @return boolean  Success. Will return FALSE if the file was already gone.
	 */
	public function deleteFile()
	{
		return unlink($this->getFullPath()) && unlink($this->getFullPathThumb());
	}

	/**
	 * Returns the storage's file as a filesystem.
	 *
	 * @return \Illuminate\Filesystem\Filesystem
	 */
	public function getAsFile()
	{
		return new File($this->getFullPath());
	}

	/**
	 * Returns the storage's thumbnail as a filesystem.
	 *
	 * @return \Illuminate\Filesystem\Filesystem
	 */
	public function getAsFileThumb()
	{
		return new File($this->getFullPathThumb());
	}

	/**
	 * Returns the attachment's base filename.
	 *
	 * @return string
	 */
	public function getBaseFileName()
	{
		$pathinfo  = pathinfo($this->pivot->filename);
		return $pathinfo['filename'];
	}

	/**
	 * Returns the storage directory, minus the file name.
	 *
	 * @return string
	 */
	public function getDirectory()
	{
		$prefix = $this->getHashPrefix($this->hash);

		return "attachments/full/{$prefix}";
	}

	/**
	 * Returns the thumbnail's storage directory, minus the file name.
	 *
	 * @return string
	 */
	public function getDirectoryThumb()
	{
		$prefix = $this->getHashPrefix($this->hash);

		return "attachments/thumb/{$prefix}";
	}

	/**
	 * Supplies a download name.
	 *
	 * @return string
	 */
	public function getDownloadName()
	{
		return "{$this->getFileName()}.{$this->guessExtension()}";
	}

	/**
	 * Supplies a clean URL for downloading an attachment on a board.
	 *
	 * @param  App\Board  $board
	 * @return string
	 */
	public function getDownloadURL(Board $board)
	{
		//return url("/{$board->board_uri}/file/{$this->pivot->attachment_id}/{$this->getDownloadName()}");

		$params = [
			'attachment' => $this->pivot->attachment_id,
			'filename'   => $this->getDownloadName(),
		];

		if (!env('APP_MEDIA_URL', false))
		{
			$params['board'] = $board;
		}

		return route('static.file.attachment', $params, false);
	}

	/**
	 * Returns the attachment's extension.
	 *
	 * @return string
	 */
	public function getExtension()
	{
		$pathinfo  = pathinfo($this->pivot->filename);
		return $pathinfo['extension'];
	}

	/**
	 * Returns the dimensions of the thumbnail, if possible.
	 *
	 * @return string|null
	 */
	public function getFileDimensions()
	{
		if ($this->has_thumbnail)
		{
			return "{$this->file_width}x{$this->file_height}";
		}

		return null;
	}

	/**
	 * Determines and returns the "xxx" of "/url/xxx.ext" for URLs.
	 *
	 * @param  string|null  $format  Optional. The token syntax for the filename. Defaults to site setting.
	 * @return string
	 */
	public function getFileName($nameFormat = null)
	{
		if (is_null($nameFormat))
		{
			// Build a thumbnail using the admin settings.
			$nameFormat = Settings::get('attachmentName');
		}

		$first_uploade_at = new \Carbon\Carbon($this->first_uploaded_at);

		$bits['t'] = $first_uploade_at->timestamp;
		$bits['i'] = 0;
		$bits['n'] = $bits['t'];

		if (isset($this->pivot))
		{
			if (isset($this->pivot->position))
			{
				$bits['i'] = $this->pivot->position;
			}

			if (isset($this->pivot->filename))
			{
				$bits['n'] = $this->getBaseFileName();
			}
		}

		$attachmentName = $nameFormat;

		foreach ($bits as $bitKey => $bitVal)
		{
			$attachmentName = str_replace("%{$bitKey}", $bitVal, $attachmentName);
		}

		return $attachmentName;
	}

	/**
	 * Returns the full internal file path.
	 *
	 * @return string
	 */
	public function getFullPath()
	{
		$storagePath = Storage::disk('local')->getDriver()->getAdapter()->getPathPrefix();
		return "{$storagePath}{$this->getPath()}";
	}

	/**
	 * Returns the full internal file path for the thumbnail.
	 *
	 * @return string
	 */
	public function getFullPathThumb()
	{
		$storagePath = Storage::disk('local')->getDriver()->getAdapter()->getPathPrefix();
		return "{$storagePath}{$this->getPathThumb()}";
	}

	/**
	 * Fetch an instance of static using the checksum.
	 *
	 * @param  $hash  Checksum
	 * @return static|null
	 */
	public static function getHash($hash)
	{
		return static::whereHash($hash)->get()->first();
	}

	/**
	 * Returns the skip file directoy prefix
	 *
	 * @param  $hash  Checksum
	 * @return static  Like "a/a/a/a"
	 */
	public static function getHashPrefix($hash)
	{
		return implode(str_split(substr($hash, 0, 4)), "/");
	}

	/**
	 * Converts the byte size to a human-readable filesize.
	 *
	 * @author <NAME>
	 * @param  int  $decimals
	 * @return string
	 */
	public function getHumanFilesize($decimals = 2)
	{
		$bytes  = $this->filesize;
		$size   = array('B', 'kiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB');
		$factor = floor((strlen($bytes) - 1) / 3);

		return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . " " . @$size[$factor];
	}

	/**
	 * Returns a URL part based on available information.
	 *
	 * @return string|int
	 */
	public function getIdentifier()
	{
		if (isset($this->pivot))
		{
			return $this->pivot->attachment_id;
		}

		return $this->hash;
	}

	/**
	 * Returns the relative internal file path.
	 *
	 * @return string
	 */
	public function getPath()
	{
		return $this->getDirectory() . "/" . $this->hash;
	}

	/**
	 * Returns the full internal file path for the thumbnail.
	 *
	 * @return string
	 */
	public function getPathThumb()
	{
		return $this->getDirectoryThumb() . "/" . $this->hash;
	}

	/**
	 * Returns a removal URL.
	 *
	 * @param  \App\Board  $board
	 * @return string
	 */
	public function getRemoveURL(Board $board)
	{
		return url("{$board->board_uri}/file/remove/{$this->pivot->attachment_id}", [], false);
	}

	/**
	 * Truncates the middle of a filename to show extension.
	 *
	 * @return string  Filename.
	 */
	public function getShortFilename()
	{
		if (isset($this->pivot) && isset($this->pivot->filename))
		{
			$filename = urldecode($this->pivot->filename);

			if (mb_strlen($filename) <= 20)
			{
				return $filename;
			}

			$ext  = pathinfo($filename, PATHINFO_EXTENSION);
			$name = pathinfo($filename, PATHINFO_FILENAME);
			$name = mb_substr($name, 0, 15);

			return "{$name}... .{$ext}";
		}

		return $this->getFileName();
	}

	/**
	 * Returns a spoiler URL.
	 *
	 * @param  \App\Board  $board
	 * @return string
	 */
	public function getSpoilerURL(Board $board)
	{
		return url("{$board->board_uri}/file/spoiler/{$this->pivot->attachment_id}", [], false);
	}

	/**
	 * Returns a string containing class names.
	 *
	 * @return string
	 */
	public function getThumbnailClasses()
	{
		$ext   = $this->guessExtension();
		$type  = "other";
		$stock = true;
		$spoil = $this->isSpoiler();

		if ($this->isImageVector())
		{
			$stock = false;
			$type  = "img";
		}
		else if ($this->isImage())
		{
			if ($this->hasThumb())
			{
				$stock = false;
				$type  = "img";
			}
		}
		else if ($this->isVideo())
		{
			if ($this->hasThumb())
			{
				$stock = false;
				$type  = "video";
			}
		}
		else if ($this->isAudio())
		{
			$stock = false;
			$type  = "audio";
		}

		$classes = [];
		$classes['type']  = "attachment-type-{$type}";
		$classes['ext']   = "attachent-ext-{$ext}";
		$classes['stock'] = $stock ? "thumbnail-stock" : "thumbnail-content";
		$classes['spoil'] = $spoil ? "thumbnail-spoiler" : "thumbnail-not-spoiler";
		$classHTML = implode(" ", $classes);

		return $classHTML;
	}

	/**
	 * Returns an XML valid attachment HTML string that handles missing thumbnail URLs.
	 *
	 * @param  \App\Board  $board  The board this thumbnail will belong to.
	 * @param  int  $maxWidth  Optional. Maximum width constraint. Defaults null.
	 * @return string  as HTML
	 */
	public function getThumbnailHTML(Board $board, $maxDimension = null)
	{
		$ext     = $this->guessExtension();
		$mime    = $this->mime;
		$url     = asset("static/img/filetypes/{$ext}.svg");
		$spoil   = $this->isSpoiler();
		$deleted = $this->isDeleted();

		if ($deleted)
		{
			$url = $board->getAssetUrl('file_deleted');
		}
		else if ($spoil)
		{
			$url = $board->getAssetUrl('file_spoiler');
		}
		else if ($this->isImageVector())
		{
			$url = $this->getDownloadURL($board);
		}
		else if ($this->isAudio() || $this->isImage() || $this->isVideo())
		{
			if ($this->hasThumb())
			{
				$url = $this->getThumbnailURL($board);
			}
			else if ($this->isAudio())
			{
				$url = asset("static/img/assets/audio.gif");
			}
		}

		$classHTML = $this->getThumbnailClasses();


		// Measure dimensions.
		$height    = "auto";
		$width     = "auto";
		$maxWidth  = "none";
		$maxHeight = "none";
		$minWidth  = "none";
		$minHeight = "none";
		$oHeight   = $this->thumbnail_height;
		$oWidth    = $this->thumbnail_width;

		if ($this->has_thumbnail && !$this->isSpoiler() && !$this->isDeleted())
		{
			$height = $oHeight . "px";
			$width  = $this->thumbnail_width . "px";

			if (is_integer($maxDimension) && ($oWidth > $maxDimension || $oHeight > $maxDimension))
			{
				if ($oWidth > $oHeight)
				{
					$height = "auto";
					$width  = $maxDimension . "px";
				}
				else if ($oWidth < $oHeight)
				{
					$height = $maxDimension . "px";
					$width  = "auto";
				}
				else
				{
					$height = $maxDimension;
					$width  = $maxDimension;
				}
			}

			$minWidth = $width;
			$minHeight = $height;
		}
		else
		{
			$maxWidth  = Settings::get('attachmentThumbnailSize') . "px";
			$maxHeight = $maxWidth;
			$width     = $maxWidth;
			$height    = "auto";

			if (is_integer($maxDimension))
			{
				$maxWidth = "{$maxDimension}px";
				$maxHeight = "{$maxDimension}px";
			}

			if ($this->isSpoiler() || $this->isDeleted())
			{
				$minHeight = "none";
				$minWidth = "none";
				$width = $maxWidth;
			}
		}

		return "<div class=\"attachment-wrapper\" style=\"height: {$height}; width: {$width};\">" .
			"<img class=\"attachment-img {$classHTML}\" src=\"{$url}\" data-mime=\"{$mime}\" style=\"height: {$height}; width: {$width};\"/>" .
		"</div>";
	}

	/**
	 * Supplies a clean thumbnail URL for embedding an attachment on a board.
	 *
	 * @param  \App\Board  $board
	 * @return string
	 */
	public function getThumbnailURL(Board $board)
	{
		$ext = $this->guessExtension();

		if ($this->isSpoiler())
		{
			return $board->getSpoilerUrl();
		}

		if ($this->isImage())
		{
			$ext = Settings::get('attachmentThumbnailJpeg') ? "jpg" : "png";
		}
		else if ($this->isVideo())
		{
			$ext = "jpg";
		}
		else if ($this->isAudio())
		{
			if (!$this->hasThumb())
			{
				return $board->getAudioArtURL();
			}

			$ext = "png";
		}
		else if ($this->isImageVector())
		{
			// With the SVG filetype, we do not generate a thumbnail, so just serve the actual SVG.
			return $this->getDownloadURL($board);
		}

		$params = [
			'attachment' => $this->getIdentifier(),
			'filename'   => $this->getDownloadName() . ".{$ext}",
		];

		if (!env('APP_MEDIA_URL', false))
		{
{'fraction_non_alphanumeric': 0.17430647291941875, 'fraction_numerical': 0.0011228533685601057, 'mean_word_length': 3.297005818078615, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 315, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 18, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4849724', 'n_tokens_mistral': 11717, 'n_tokens_neox': 10311, 'n_words': 2302}