Skip to content

Commit

Permalink
Merge pull request #32 from Planetbiru/feature/2.6
Browse files Browse the repository at this point in the history
Feature/2.6
  • Loading branch information
kamshory authored Nov 8, 2024
2 parents fa25371 + 0ce1414 commit 13e9ff3
Show file tree
Hide file tree
Showing 5 changed files with 326 additions and 104 deletions.
181 changes: 181 additions & 0 deletions src/File/PicoDownloadFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
<?php

namespace MagicObject\File;

/**
* Class PicoDownloadFile
*
* Facilitates downloading a file, with support for partial content (range requests).
* This class ensures that requested files exist, handles errors, and supports downloading large files
* efficiently by sending them in chunks.
*
* @author Kamshory
* @package MagicObject\File
* @link https://github.com/Planetbiru/MagicObject
*/
class PicoDownloadFile
{
/**
* @var string The path to the file being downloaded.
*/
private $filepath;

/**
* @var string The filename to be used in the download response.
*/
private $filename;

/**
* PicoDownloadFile constructor.
*
* @param string $filepath The full path to the file.
* @param string|null $filename The name of the file for download (optional).
*/
public function __construct($filepath, $filename = null)
{
$this->filepath = $filepath;
$this->filename = $filename ?: basename($filepath); // Use basename if no filename provided
}

/**
* Initiates the download of the file with support for partial content (range requests).
*
* Handles the following:
* - Verifies the file exists at the specified path.
* - Supports byte range requests for resuming downloads.
* - Sends appropriate HTTP headers for file transfer.
* - Streams the file to the client in chunks (8 KB by default).
*
* @param bool $exit Whether to terminate the script after sending the file. Default is `false`.
*
* @return bool Returns `true` if the entire file was successfully sent, `false` if only part of the file was sent.
*/
public function download($exit = false) //NOSONAR
{
if (!$this->fileExists()) {
$this->sendError(404, "File not found.");
return false;
}

$fileSize = filesize($this->filepath);
list($start, $end) = $this->getRange($fileSize);

if ($this->isInvalidRange($start, $end, $fileSize)) {
$this->sendError(416, "Range Not Satisfiable", $fileSize);
return false;
}

$this->sendHeaders($start, $end, $fileSize);

$fp = fopen($this->filepath, 'rb');
if ($fp === false) {
$this->sendError(500, "Failed to open file.");
return false;
}

$this->streamFile($fp, $start, $end);

fclose($fp);

if ($exit) {
exit;
}

return $end === ($fileSize - 1); // Return true if the whole file was sent
}

/**
* Checks if the file exists.
*
* @return bool True if the file exists, false otherwise.
*/
private function fileExists()
{
return file_exists($this->filepath);
}

/**
* Sends an error response with the given status code and message.
*
* @param int $statusCode The HTTP status code.
* @param string $message The error message.
* @param int|null $fileSize The file size to include in the Content-Range header (optional).
*/
private function sendError($statusCode, $message, $fileSize = null)
{
header("HTTP/1.1 $statusCode $message");
if ($fileSize !== null) {
header("Content-Range: bytes 0-0/$fileSize");
}
echo $message;
}

/**
* Determines the byte range from the HTTP_RANGE header.
*
* @param int $fileSize The size of the file.
* @return array The start and end byte positions for the range.
*/
private function getRange($fileSize)
{
if (isset($_SERVER['HTTP_RANGE'])) {
list($range, $extra) = explode(',', $_SERVER['HTTP_RANGE'], 2); //NOSONAR
list($start, $end) = explode('-', $range);
$start = max(0, (int)$start);
$end = $end ? (int)$end : $fileSize - 1;
} else {
$start = 0;
$end = $fileSize - 1;
}

return [$start, $end];
}

/**
* Checks if the byte range is valid.
*
* @param int $start The start byte.
* @param int $end The end byte.
* @param int $fileSize The total size of the file.
* @return bool True if the range is invalid.
*/
private function isInvalidRange($start, $end, $fileSize)
{
return $start > $end || $start >= $fileSize || $end >= $fileSize;
}

/**
* Sends the appropriate HTTP headers for the download.
*
* @param int $start The start byte.
* @param int $end The end byte.
* @param int $fileSize The total size of the file.
*/
private function sendHeaders($start, $end, $fileSize)
{
header('HTTP/1.1 206 Partial Content');
header("Content-Type: application/octet-stream");
header("Content-Description: File Transfer");
header("Content-Disposition: attachment; filename=\"" . $this->filename . "\"");
header("Content-Range: bytes $start-$end/$fileSize");
header("Content-Length: " . ($end - $start + 1));
header("Accept-Ranges: bytes");
}

/**
* Streams the file to the client in chunks.
*
* @param resource $fp The file pointer.
* @param int $start The start byte.
* @param int $end The end byte.
*/
private function streamFile($fp, $start, $end)
{
$bufferSize = 1024 * 8; // 8 KB buffer size
fseek($fp, $start);
while (!feof($fp) && ftell($fp) <= $end) {
echo fread($fp, $bufferSize);
flush();
}
}
}
4 changes: 2 additions & 2 deletions src/Geometry/Area.php
Original file line number Diff line number Diff line change
Expand Up @@ -172,12 +172,12 @@ public function getHTML()
$attrs[] = 'coords="' . implode(", ", $this->getCoords($this->zoom)) . '"';

if (isset($this->href)) {
$attrs[] = 'href="' . htmlspecialchars($this->href) . '"';
$attrs[] = 'href="' . $this->href . '"';
}

if (isset($this->attributes) && is_array($this->attributes)) {
foreach ($this->attributes as $key => $value) {
$attrs[] = $key . '="' . htmlspecialchars($value) . '"';
$attrs[] = $key . '="' . $value . '"';
}
}

Expand Down
57 changes: 49 additions & 8 deletions src/Txt.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,65 @@
/**
* Class Txt
*
* A utility class that provides dynamic handling of static method calls.
* This class allows for flexible interaction by returning the names of
* methods that are called statically but are not explicitly defined within
* the class. It can be useful for implementing dynamic behavior or
* creating a fluent interface.
* A utility class that provides dynamic handling of static method calls and dynamic property access.
* This class allows for flexible interaction by returning the names of methods and properties
* that are called statically or accessed dynamically but are not explicitly defined within the class.
* It can be useful for implementing dynamic behavior or creating a fluent interface.
*/
class Txt
{
/**
* Handles static calls to undefined methods.
*
* This method returns the name of the method being called.
* This method intercepts calls to static methods that are not explicitly defined in the class
* and returns the name of the method being called. It allows for flexible handling of undefined
* static methods.
*
* @param string $name The name of the method being called.
* @param array $arguments An array of arguments passed to the method.
* @return string|null The name of the called method, or null if no arguments are provided.
* @return string The name of the called method.
*/
public static function __callStatic($name, $arguments)
public static function __callStatic($name, $arguments) //NOSONAR
{
return $name;
}

/**
* Returns a new instance of the Txt class.
*
* This method allows you to retrieve an instance of the Txt class for non-static operations.
* This instance can be used to access dynamic properties via the __get() magic method.
*
* @return Txt A new instance of the Txt class.
*/
public static function getInstance()
{
return new self;
}

/**
* Creates and returns a new instance of the Txt class.
*
* Similar to getInstance(), this method allows you to retrieve an instance of the Txt class
* for non-static operations, such as dynamic property access using the __get() magic method.
*
* @return Txt A new instance of the Txt class.
*/
public static function of()
{
return new self;
}

/**
* Handles dynamic access to undefined properties.
*
* This method is invoked when an undefined property is accessed on an instance of the Txt class.
* It returns the name of the property being accessed.
*
* @param string $name The name of the property being accessed.
* @return string The name of the accessed property.
*/
public function __get($name)
{
return $name;
}
Expand Down
5 changes: 5 additions & 0 deletions src/Util/PicoLocale.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
/**
* Locale
*
* The `PicoLocale` class serves as a container for a collection of predefined locale constants.
* These constants represent different locale identifiers for various countries, regions, and languages, using the format of language-region (e.g., "en_US" for English in the United States, "fr_FR" for French in France).
*
* Each constant corresponds to a specific locale, which can be used in applications to handle internationalization (i18n), localization (l10n), and formatting of data such as dates, currencies, and numbers, based on the user's regional settings.
*
* @author Kamshory
* @package MagicObject\Util
* @link https://github.com/Planetbiru/MagicObject
Expand Down
Loading

0 comments on commit 13e9ff3

Please sign in to comment.