Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.com.git

Writes lock files to temp directory

Author Dave Jarvis <email>
Date 2026-03-03 19:42:50 GMT-0800
Commit fc8967085823a4e7f04a37d3fc4eb4bea8d4e6d1
Parent 79bc11b
downloads/counter.php
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' );
?>
Delta 419 lines added, 280 lines removed, 139-line increase