| | ignore_user_abort( true ); |
| | |
| | - $level = ob_get_level(); |
| | - $cleared = false; |
| | - |
| | - while( $level > 0 && !$cleared ) { |
| | - ob_end_clean(); |
| | - |
| | - $newLevel = ob_get_level(); |
| | - |
| | - if( $newLevel === $level ) { |
| | - $cleared = true; |
| | - } |
| | - else { |
| | - $level = $newLevel; |
| | - } |
| | - } |
| | - |
| | - if( session_id() === "" ) { |
| | - session_start(); |
| | - } |
| | - |
| | - class DownloadManager { |
| | - private $filename; |
| | - private $expiry; |
| | - |
| | - public function __construct( $filename, $expiry ) { |
| | - $this->filename = $filename; |
| | - $this->expiry = $expiry; |
| | - } |
| | - |
| | - public function process() { |
| | - $result = false; |
| | - |
| | - if( $this->filename !== '' ) { |
| | - if( $this->tokenExpired() ) { |
| | - $this->incrementCount(); |
| | - } |
| | - |
| | - $result = $this->download(); |
| | - } |
| | - |
| | - return $result; |
| | - } |
| | - |
| | - /** |
| | - * Downloads a file, allowing for resuming partial downloads. |
| | - * |
| | - * @return bool True if the file was transferred. |
| | - */ |
| | - private function download() { |
| | - $result = false; |
| | - $method = isset( $_SERVER[ 'REQUEST_METHOD' ] ) |
| | - ? $_SERVER[ 'REQUEST_METHOD' ] |
| | - : 'GET'; |
| | - $isHead = $method === 'HEAD'; |
| | - $contentType = mime_content_type( $this->filename ); |
| | - |
| | - clearstatcache(); |
| | - |
| | - $fileSize = @filesize( $this->filename ); |
| | - $size = $fileSize === false || empty( $fileSize ) ? 0 : $fileSize; |
| | - |
| | - if( !$isHead ) { |
| | - $rangeData = $this->parseRange( $size ); |
| | - $start = $rangeData[ 0 ]; |
| | - $length = $rangeData[ 1 ]; |
| | - |
| | - if( $length > 0 ) { |
| | - header_remove( 'x-powered-by' ); |
| | - header( 'Expires: 0' ); |
| | - header( |
| | - 'Cache-Control: public, must-revalidate, post-check=0, pre-check=0' |
| | - ); |
| | - header( 'Cache-Control: private', false ); |
| | - header( |
| | - 'Content-Disposition: attachment; filename="' . |
| | - $this->filename . '"' |
| | - ); |
| | - header( 'Accept-Ranges: bytes' ); |
| | - header( "Content-Length: $length" ); |
| | - header( "Content-Type: $contentType" ); |
| | - |
| | - $result = $this->transmit( $start, $size ); |
| | - } |
| | - } |
| | - |
| | - return $result; |
| | - } |
| | - |
| | - /** |
| | - * Parses the HTTP range request header, provided one was sent by the |
| | - * client. This provides download resume functionality. |
| | - * |
| | - * @param int $size The total file size (as stored on disk). |
| | - * |
| | - * @return array The starting offset for resuming the download, or 0 to |
| | - * download the entire file (i.e., no offset could be parsed); also the |
| | - * number of bytes to be transferred. |
| | - */ |
| | - private function parseRange( $size ) { |
| | - $start = 0; |
| | - $length = $size; |
| | - |
| | - if( isset( $_SERVER[ 'HTTP_RANGE' ] ) ) { |
| | - $format = '/^bytes=(\d*)-(\d*)(?:,\d*-\d*)*$/'; |
| | - $range = $_SERVER[ 'HTTP_RANGE' ]; |
| | - $match = preg_match( $format, $range, $matches ); |
| | - |
| | - if( $match ) { |
| | - $start = isset( $matches[ 1 ] ) ? (int)$matches[ 1 ] : 0; |
| | - $end = !empty( $matches[ 2 ] ) ? (int)$matches[ 2 ] : $size - 1; |
| | - $length = $end - $start + 1; |
| | - |
| | - header( 'HTTP/1.1 206 Partial Content' ); |
| | - header( "Content-Range: bytes $start-$end/$size" ); |
| | - } |
| | - else { |
| | - header( 'HTTP/1.1 416 Requested Range Not Satisfiable' ); |
| | - header( "Content-Range: bytes */$size" ); |
| | - |
| | - $length = 0; |
| | - } |
| | - } |
| | - |
| | - return array( |
| | - $start, |
| | - $length |
| | - ); |
| | - } |
| | - |
| | - /** |
| | - * Transmits a file from the server to the client. |
| | - * |
| | - * @param int $seekStart Offset into file to start downloading. |
| | - * @param int $size Total size of the file. |
| | - * |
| | - * @return bool True if the file was transferred. |
| | - */ |
| | - private function transmit( $seekStart, $size ) { |
| | - $result = false; |
| | - $bytesSent = -1; |
| | - $fp = @fopen( $this->filename, 'rb' ); |
| | - |
| | - if( $fp !== false ) { |
| | - $aborted = false; |
| | - $bytesSent = $seekStart; |
| | - $chunkSize = 1024 * 16; |
| | - |
| | - @fseek( $fp, $seekStart ); |
| | - |
| | - while( !feof( $fp ) && !$aborted ) { |
| | - print( @fread( $fp, $chunkSize ) ); |
| | - |
| | - $bytesSent += $chunkSize; |
| | - $aborted = connection_aborted() || connection_status() !== 0; |
| | - |
| | - flush(); |
| | - } |
| | - |
| | - fclose( $fp ); |
| | - |
| | - $result = $bytesSent >= $size; |
| | - } |
| | - |
| | - return $result; |
| | - } |
| | - |
| | - /** |
| | - * Answers whether the user's download token has expired. |
| | - * |
| | - * @return bool True indicates the token has expired (or was not set). |
| | - */ |
| | - private function tokenExpired() { |
| | - $tokenName = 'LAST_DOWNLOAD'; |
| | - $tokenCreate = 'CREATED'; |
| | - $now = time(); |
| | - $expired = !isset( $_SESSION[ $tokenName ] ); |
| | - |
| | - if( !$expired && $now - $_SESSION[ $tokenName ] > $this->expiry ) { |
| | - $expired = true; |
| | - $_SESSION = array(); |
| | - |
| | - session_destroy(); |
| | - session_start(); |
| | - } |
| | - |
| | - $_SESSION[ $tokenName ] = $now; |
| | - |
| | - if( !isset( $_SESSION[ $tokenCreate ] ) ) { |
| | - $_SESSION[ $tokenCreate ] = $now; |
| | - } |
| | - else { |
| | - $elapsed = $now - $_SESSION[ $tokenCreate ]; |
| | - |
| | - if( $elapsed > $this->expiry ) { |
| | - session_regenerate_id( true ); |
| | - |
| | - $_SESSION[ $tokenCreate ] = $now; |
| | - } |
| | - } |
| | - |
| | - return $expired; |
| | - } |
| | - |
| | - /** |
| | - * Increments the number in a file using an exclusive lock. The file |
| | - * is set to an initial value set to 0 if it doesn't exist. |
| | - */ |
| | - private function incrementCount() { |
| | - $result = false; |
| | - $filename = $this->filename . '-count.txt'; |
| | - $lockDir = $filename . '.lock'; |
| | - $locked = $this->lockOpen( $lockDir ); |
| | - |
| | - if( $locked ) { |
| | - $count = (int)@file_get_contents( $filename ) + 1; |
| | - |
| | - @file_put_contents( $filename, $count ); |
| | - @rmdir( $lockDir ); |
| | - |
| | - $result = true; |
| | - } |
| | - |
| | - return $result; |
| | - } |
| | - |
| | - /** |
| | - * Acquires a lock for a particular file. Callers would be prudent to |
| | - * call this function from within a try/finally block and close the lock |
| | - * in the finally section. The amount of time between opening and closing |
| | - * the lock must be minimal because parallel processes will be waiting on |
| | - * the lock's release. |
| | - * |
| | - * @param string $lockDir The name of directory to lock. |
| | - * |
| | - * @return bool True if the lock was obtained, false upon excessive |
| | - * attempts. |
| | - */ |
| | - private function lockOpen( $lockDir ) { |
| | - $result = false; |
| | - $iterations = 0; |
| | - |
| | - while( $iterations < 10 && !$result ) { |
| | - $result = @mkdir( $lockDir, 0777 ); |
| | - |
| | - if( !$result ) { |
| | - $iterations++; |
| | - $lifetime = time() - (int)@filemtime( $lockDir ); |
| | - |
| | - if( $lifetime > 10 ) { |
| | - @rmdir( $lockDir ); |
| | - } |
| | - else { |
| | - usleep( rand( 1000, 10000 ) ); |
| | - } |
| | - } |
| | - } |
| | - |
| | - return $result; |
| | - } |
| | - } |
| | - |
| | - /** |
| | - * Retrieve the file name being downloaded from the HTTP GET request. |
| | - * |
| | - * @return string The sanitized file name (without path information). |
| | - */ |
| | - function getSanitizedFilename() { |
| | - $filepath = isset( $_GET[ 'filename' ] ) ? $_GET[ 'filename' ] : ''; |
| | - $fileinfo = pathinfo( $filepath ); |
| | - $basename = $fileinfo[ 'basename' ]; |
| | - $noSpaces = preg_replace( '/\s+/', '', $basename ); |
| | - |
| | - return preg_replace( '/[^\w\d\-_~,;\[\]\(\).]/', '', $noSpaces ); |
| | - } |
| | - |
| | - $filename = getSanitizedFilename(); |
| | - $expiry = 24 * 60 * 60; |
| | - $manager = new DownloadManager( $filename, $expiry ); |
| | - |
| | - $manager->process(); |
| | + /** |
| | + * Logs a message to the PHP error log with a consistent prefix. |
| | + * |
| | + * @param string $message The message to log. |
| | + */ |
| | + function dl_log( $message ) { |
| | + $timestamp = date( 'Y-m-d H:i:s' ); |
| | + $sessionId = session_id() ?: 'none'; |
| | + error_log( "[DownloadManager][$timestamp][session=$sessionId] $message" ); |
| | + } |
| | + |
| | + dl_log( 'Script invoked. REQUEST_URI=' . ($_SERVER['REQUEST_URI'] ?? 'unknown') ); |
| | + |
| | + $level = ob_get_level(); |
| | + $cleared = false; |
| | + |
| | + while( $level > 0 && !$cleared ) { |
| | + ob_end_clean(); |
| | + |
| | + $newLevel = ob_get_level(); |
| | + |
| | + if( $newLevel === $level ) { |
| | + $cleared = true; |
| | + } |
| | + else { |
| | + $level = $newLevel; |
| | + } |
| | + } |
| | + |
| | + dl_log( "Output buffering cleared (level was $level)" ); |
| | + |
| | + if( session_id() === "" ) { |
| | + session_start(); |
| | + dl_log( 'Session started: ' . session_id() ); |
| | + } |
| | + else { |
| | + dl_log( 'Session already active: ' . session_id() ); |
| | + } |
| | + |
| | + class DownloadManager { |
| | + private $filename; |
| | + private $expiry; |
| | + private $dataDir; |
| | + |
| | + /** |
| | + * Retrieve and sanitize the file name being downloaded from the HTTP |
| | + * GET request (without path information). |
| | + * |
| | + * @param string $filename The unsanitized file name. |
| | + * @param int $expiry The expiration time in seconds. |
| | + * @param string $dataDir Writable directory for count and lock files. |
| | + */ |
| | + public function __construct( $filename, $expiry, $dataDir = '/tmp/download-counts' ) { |
| | + dl_log( "Constructor called with filename='$filename', expiry=$expiry" ); |
| | + |
| | + $fileinfo = pathinfo( $filename ); |
| | + $basename = isset( $fileinfo[ 'basename' ] ) |
| | + ? $fileinfo[ 'basename' ] |
| | + : ''; |
| | + $noSpaces = preg_replace( '/\s+/', '', $basename ); |
| | + $regex = '/[^\w\d\-_~,;\[\]\(\).]/'; |
| | + $clean = preg_replace( $regex, '', $noSpaces ); |
| | + $this->filename = is_string( $clean ) ? $clean : ''; |
| | + $this->expiry = $expiry; |
| | + $this->dataDir = rtrim( $dataDir, '/' ); |
| | + |
| | + if( !is_dir( $this->dataDir ) ) { |
| | + @mkdir( $this->dataDir, 0777, true ); |
| | + dl_log( "Created data directory: '$this->dataDir'" ); |
| | + } |
| | + |
| | + dl_log( "Sanitized filename='$this->filename' (raw basename='$basename'), dataDir='$this->dataDir'" ); |
| | + |
| | + if( $this->filename === '' ) { |
| | + dl_log( 'WARNING: Filename is empty after sanitization' ); |
| | + } |
| | + } |
| | + |
| | + public function process() { |
| | + $result = false; |
| | + |
| | + if( $this->filename !== '' ) { |
| | + dl_log( "Processing download for '$this->filename'" ); |
| | + |
| | + $expired = $this->tokenExpired(); |
| | + |
| | + if( $expired ) { |
| | + dl_log( "Token expired for '$this->filename', incrementing count" ); |
| | + $incremented = $this->incrementCount(); |
| | + dl_log( "Count increment result: " . ($incremented ? 'success' : 'failure') ); |
| | + } |
| | + else { |
| | + dl_log( "Token still valid for '$this->filename', skipping count increment" ); |
| | + } |
| | + |
| | + session_write_close(); |
| | + dl_log( 'Session closed, starting download' ); |
| | + |
| | + $result = $this->download(); |
| | + |
| | + dl_log( "Download result for '$this->filename': " . ($result ? 'success' : 'failure') ); |
| | + } |
| | + else { |
| | + dl_log( 'Skipping process: filename is empty' ); |
| | + } |
| | + |
| | + return $result; |
| | + } |
| | + |
| | + /** |
| | + * Downloads a file, allowing for resuming partial downloads. |
| | + * |
| | + * @return bool True if the file was transferred. |
| | + */ |
| | + private function download() { |
| | + $result = false; |
| | + $req = 'REQUEST_METHOD'; |
| | + $method = isset( $_SERVER[ $req ] ) ? $_SERVER[ $req ] : 'GET'; |
| | + $isHead = $method === 'HEAD'; |
| | + $mime = @mime_content_type( $this->filename ); |
| | + $mimeType = is_string( $mime ) ? $mime : 'application/octet-stream'; |
| | + |
| | + dl_log( "Download method=$method, isHead=" . ($isHead ? 'true' : 'false') . ", mimeType=$mimeType" ); |
| | + |
| | + clearstatcache(); |
| | + |
| | + $fileSize = @filesize( $this->filename ); |
| | + $size = $fileSize === false ? 0 : $fileSize; |
| | + |
| | + dl_log( "File size for '$this->filename': $size bytes" ); |
| | + |
| | + if( $size === 0 ) { |
| | + dl_log( "WARNING: File size is 0 or file not found: '$this->filename'" ); |
| | + } |
| | + |
| | + if( !$isHead ) { |
| | + $rangeData = $this->parseRange( $size ); |
| | + $start = $rangeData[ 0 ]; |
| | + $length = $rangeData[ 1 ]; |
| | + |
| | + dl_log( "Range parsed: start=$start, length=$length" ); |
| | + |
| | + if( $length > 0 ) { |
| | + $cache = 'Cache-Control: public, must-revalidate, '; |
| | + $cache .= 'post-check=0, pre-check=0'; |
| | + |
| | + header_remove( 'x-powered-by' ); |
| | + header( 'Expires: 0' ); |
| | + header( $cache ); |
| | + header( 'Cache-Control: private', false ); |
| | + header( |
| | + 'Content-Disposition: attachment; filename="' . |
| | + $this->filename . '"' |
| | + ); |
| | + header( 'Accept-Ranges: bytes' ); |
| | + header( "Content-Length: $length" ); |
| | + header( "Content-Type: $mimeType" ); |
| | + |
| | + dl_log( "Headers sent, beginning transmit: start=$start, size=$size" ); |
| | + |
| | + $result = $this->transmit( $start, $size ); |
| | + } |
| | + else { |
| | + dl_log( 'Skipping transmit: length is 0' ); |
| | + } |
| | + } |
| | + else { |
| | + dl_log( 'HEAD request, skipping file transfer' ); |
| | + } |
| | + |
| | + return $result; |
| | + } |
| | + |
| | + /** |
| | + * Parses the HTTP range request header, provided one was sent by the |
| | + * client. This provides download resume functionality. |
| | + * |
| | + * @param int $size The total file size (as stored on disk). |
| | + * |
| | + * @return array The starting offset for resuming the download, or 0 to |
| | + * download the entire file (i.e., no offset could be parsed); also the |
| | + * number of bytes to be transferred. |
| | + */ |
| | + private function parseRange( $size ) { |
| | + $start = 0; |
| | + $length = $size; |
| | + |
| | + if( isset( $_SERVER[ 'HTTP_RANGE' ] ) ) { |
| | + $format = '/^bytes=(\d*)-(\d*)(?:,\d*-\d*)*$/'; |
| | + $range = $_SERVER[ 'HTTP_RANGE' ]; |
| | + $match = preg_match( $format, $range, $matches ); |
| | + |
| | + dl_log( "Range header present: '$range'" ); |
| | + |
| | + if( $match === 1 ) { |
| | + $start = isset( $matches[ 1 ] ) ? (int)$matches[ 1 ] : 0; |
| | + $end = !empty( $matches[ 2 ] ) ? (int)$matches[ 2 ] : $size - 1; |
| | + $length = $end - $start + 1; |
| | + |
| | + header( 'HTTP/1.1 206 Partial Content' ); |
| | + header( "Content-Range: bytes $start-$end/$size" ); |
| | + |
| | + dl_log( "Partial content: bytes $start-$end/$size (length=$length)" ); |
| | + } |
| | + else { |
| | + header( 'HTTP/1.1 416 Requested Range Not Satisfiable' ); |
| | + header( "Content-Range: bytes */$size" ); |
| | + |
| | + dl_log( "Range not satisfiable for: '$range'" ); |
| | + |
| | + $length = 0; |
| | + } |
| | + } |
| | + else { |
| | + dl_log( 'No range header, full download requested' ); |
| | + } |
| | + |
| | + return array( |
| | + $start, |
| | + $length |
| | + ); |
| | + } |
| | + |
| | + /** |
| | + * Transmits a file from the server to the client. |
| | + * |
| | + * @param int $seekStart Offset into file to start downloading. |
| | + * @param int $size Total size of the file. |
| | + * |
| | + * @return bool True if the file was transferred. |
| | + */ |
| | + private function transmit( $seekStart, $size ) { |
| | + $result = false; |
| | + $fp = @fopen( $this->filename, 'rb' ); |
| | + |
| | + if( $fp !== false ) { |
| | + $bytesSent = $seekStart; |
| | + $chunkSize = 1024 * 16; |
| | + $aborted = false; |
| | + |
| | + @fseek( $fp, $seekStart ); |
| | + |
| | + dl_log( "Transmitting '$this->filename' from offset $seekStart (total size $size)" ); |
| | + |
| | + while( !feof( $fp ) && !$aborted ) { |
| | + print( @fread( $fp, $chunkSize ) ); |
| | + |
| | + $bytesSent += $chunkSize; |
| | + $aborted = connection_aborted() || connection_status() !== 0; |
| | + |
| | + flush(); |
| | + } |
| | + |
| | + fclose( $fp ); |
| | + |
| | + $result = $bytesSent >= $size; |
| | + |
| | + if( $aborted ) { |
| | + dl_log( "Transfer aborted at byte $bytesSent of $size for '$this->filename'" ); |
| | + } |
| | + else { |
| | + dl_log( "Transfer complete: $bytesSent bytes sent for '$this->filename' (result=" . ($result ? 'success' : 'failure') . ")" ); |
| | + } |
| | + } |
| | + else { |
| | + dl_log( "ERROR: Could not open file '$this->filename' for reading" ); |
| | + } |
| | + |
| | + return $result; |
| | + } |
| | + |
| | + /** |
| | + * Answers whether the user's download token has expired. |
| | + * |
| | + * @return bool True indicates the token has expired (or was not set). |
| | + */ |
| | + private function tokenExpired() { |
| | + $tokenName = 'LAST_DOWNLOAD_' . $this->filename; |
| | + $tokenCreate = 'CREATED_' . $this->filename; |
| | + $now = time(); |
| | + $expired = !isset( $_SESSION[ $tokenName ] ); |
| | + |
| | + if( $expired ) { |
| | + dl_log( "Token '$tokenName' not set in session (first download)" ); |
| | + } |
| | + |
| | + if( !$expired && $now - $_SESSION[ $tokenName ] > $this->expiry ) { |
| | + $elapsed = $now - $_SESSION[ $tokenName ]; |
| | + dl_log( "Token '$tokenName' expired: elapsed=${elapsed}s > expiry=$this->expiry" . "s" ); |
| | + $expired = true; |
| | + } |
| | + |
| | + $_SESSION[ $tokenName ] = $now; |
| | + |
| | + if( !isset( $_SESSION[ $tokenCreate ] ) ) { |
| | + $_SESSION[ $tokenCreate ] = $now; |
| | + dl_log( "Session creation token set for '$this->filename'" ); |
| | + } |
| | + else { |
| | + $elapsed = $now - $_SESSION[ $tokenCreate ]; |
| | + |
| | + if( $elapsed > $this->expiry ) { |
| | + session_regenerate_id( true ); |
| | + dl_log( "Session ID regenerated for '$this->filename' (elapsed=${elapsed}s)" ); |
| | + |
| | + $_SESSION[ $tokenCreate ] = $now; |
| | + } |
| | + } |
| | + |
| | + dl_log( "Token expired result for '$this->filename': " . ($expired ? 'true' : 'false') ); |
| | + |
| | + return $expired; |
| | + } |
| | + |
| | + /** |
| | + * Increments the number in a file using an exclusive lock. The file |
| | + * is set to an initial value set to 0 if it doesn't exist. |
| | + */ |
| | + private function incrementCount() { |
| | + $result = false; |
| | + $filename = $this->dataDir . '/' . $this->filename . '-count.txt'; |
| | + $lockDir = $this->dataDir . '/' . $this->filename . '-count.txt.lock'; |
| | + |
| | + dl_log( "Attempting to increment count in '$filename'" ); |
| | + |
| | + $locked = $this->lockOpen( $lockDir ); |
| | + |
| | + if( $locked ) { |
| | + $raw = @file_get_contents( $filename ); |
| | + $count = (int)$raw + 1; |
| | + |
| | + dl_log( "Count for '$this->filename': $raw -> $count" ); |
| | + |
| | + @file_put_contents( $filename, $count ); |
| | + @rmdir( $lockDir ); |
| | + |
| | + dl_log( "Lock released for '$filename'" ); |
| | + |
| | + $result = true; |
| | + } |
| | + else { |
| | + dl_log( "ERROR: Could not acquire lock for '$filename'" ); |
| | + } |
| | + |
| | + return $result; |
| | + } |
| | + |
| | + /** |
| | + * Acquires a lock for a particular file. Callers would be prudent to |
| | + * call this function from within a try/finally block and close the lock |
| | + * in the finally section. The amount of time between opening and closing |
| | + * the lock must be minimal because parallel processes will be waiting on |
| | + * the lock's release. |
| | + * |
| | + * @param string $lockDir The name of directory to lock. |
| | + * |
| | + * @return bool True if the lock was obtained, false upon excessive |
| | + * attempts. |
| | + */ |
| | + private function lockOpen( $lockDir ) { |
| | + $result = false; |
| | + $iterations = 0; |
| | + |
| | + dl_log( "Acquiring lock: '$lockDir'" ); |
| | + |
| | + while( $iterations < 10 && !$result ) { |
| | + $result = @mkdir( $lockDir, 0777 ); |
| | + |
| | + if( !$result ) { |
| | + $iterations++; |
| | + $lastErr = error_get_last(); |
| | + $errMsg = $lastErr ? $lastErr['message'] : 'unknown'; |
| | + $dirExists = is_dir( $lockDir ); |
| | + |
| | + dl_log( "mkdir failed (attempt $iterations): dir_exists=" . |
| | + ($dirExists ? 'true' : 'false') . |
| | + ", error='$errMsg', cwd='" . getcwd() . "'" ); |
| | + |
| | + if( $dirExists ) { |
| | + $lifetime = time() - (int)@filemtime( $lockDir ); |
| | + |
| | + if( $lifetime > 10 ) { |
| | + dl_log( "Stale lock detected (age=${lifetime}s), removing '$lockDir'" ); |
| | + @rmdir( $lockDir ); |
| | + } |
| | + else { |
| | + $sleepUs = rand( 1000, 10000 ); |
| | + dl_log( "Lock contention on attempt $iterations, sleeping ${sleepUs}us" ); |
| | + usleep( $sleepUs ); |
| | + } |
| | + } |
| | + else { |
| | + dl_log( "Lock dir does not exist but mkdir failed — likely a permissions issue on parent directory" ); |
| | + break; |
| | + } |
| | + } |
| | + } |
| | + |
| | + if( !$result ) { |
| | + dl_log( "WARNING: Failed to acquire lock after $iterations attempts: '$lockDir'" ); |
| | + } |
| | + else { |
| | + dl_log( "Lock acquired: '$lockDir'" ); |
| | + } |
| | + |
| | + return $result; |
| | + } |
| | + } |
| | + |
| | + $filename = isset( $_GET[ 'filename' ] ) ? $_GET[ 'filename' ] : ''; |
| | + $expiry = 24 * 60 * 60; |
| | + |
| | + dl_log( "GET filename parameter: '$filename'" ); |
| | + |
| | + $manager = new DownloadManager( $filename, $expiry ); |
| | + |
| | + $manager->process(); |
| | + |
| | + dl_log( 'Script finished' ); |
| | ?> |
| | |