#!/usr/bin/env php | |
<?php | |
/** | |
* $extractPath must be writable by the user account used to launch this script | |
*/ | |
$extractPath = '/opt/.phpstorm'; | |
$versionPage = 'https://data.services.jetbrains.com/products/releases?code=PS&latest=true&type=release&_=' . time(); | |
#region Functions | |
// get the page text - uses a cached copy if retrieved within the last 5 minutes | |
$getPage = function ($url) { | |
$file = sys_get_temp_dir() . '/phpstorm-' . dechex(crc32($url)) . '.html'; | |
$splFileInfo = new \SplFileInfo($file); | |
$modified = $splFileInfo->isFile() ? $splFileInfo->getMTime() : 0; | |
if (time() - $modified > 300) { | |
file_put_contents($file, file_get_contents($url)); | |
} | |
return file_get_contents($file); | |
}; | |
$getInstalledVersion = function () use ($extractPath) { | |
$version = null; | |
$file = "$extractPath/default/build.txt"; | |
if (file_exists($file)) { | |
$version = preg_replace('/^PS-/', '', trim(file_get_contents($file))); | |
} | |
return $version; | |
}; | |
$getJson = function ($url) use ($getPage) { | |
if (($json = json_decode($getPage($url), true)) === false) { | |
fwrite(STDERR, "Failed to decode JSON\n"); | |
exit(1); | |
} | |
return $json; | |
}; | |
$getJsonProperty = function ($url, $property) use ($getJson) { | |
$json = $getJson($url); | |
foreach (explode('.', $property) as $key) { | |
if (!array_key_exists($key, $json)) { | |
fwrite(STDERR, "Invalid JSON property\n"); | |
exit(1); | |
} | |
$json = $json[$key]; | |
} | |
return $json; | |
}; | |
$getLatestVersion = function () use ($getJsonProperty, $versionPage) { | |
return $getJsonProperty($versionPage, 'PS.0.build'); | |
}; | |
$getDownloadLink = function () use ($getJsonProperty, $versionPage) { | |
return $getJsonProperty($versionPage, 'PS.0.downloads.linux.link'); | |
}; | |
$downloadFile = function ($url, $destination, $callback = null, &$info = array()) { | |
$hnd = fopen($destination, 'wb'); | |
$ch = curl_init($url); | |
curl_setopt($ch, CURLOPT_FILE, $hnd); | |
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); | |
if ($callback) { | |
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $bytes) use ($callback, $hnd) { | |
return call_user_func($callback, $ch, $bytes, $hnd); | |
}); | |
} | |
curl_exec($ch); | |
$info = curl_getinfo($ch); | |
curl_close($ch); | |
fclose($hnd); | |
if ($info['http_code'] === 200) { | |
$filesize = filesize($destination); | |
} else { | |
if (file_exists($destination)) { | |
unlink($destination); | |
} | |
$filesize = -1; | |
} | |
return $filesize; | |
}; | |
$downloadFileWithProgress = function ( | |
$url, | |
$destination, | |
$progressCallback = null, | |
$interval = 100, | |
&$info = array()) use ($downloadFile) { | |
if (function_exists('pcntl_fork')) { | |
$key = ftok(__FILE__, 't'); | |
$shm = @shm_attach($key, 102400, 0640); | |
} else { | |
$key = null; | |
$shm = null; | |
} | |
if ($shm) { | |
shm_put_var($shm, 0, null); // waiting for the child to finish | |
shm_put_var($shm, 1, -1); // the filesize of the remote target | |
shm_put_var($shm, 2, -1); // the number or bytes written | |
$pid = pcntl_fork(); | |
if ($pid === 0) { // child fork | |
$pid = posix_getpid(); | |
try { | |
// perform a HEAD request to get the size of the remote file | |
$ch = curl_init($url); | |
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); | |
curl_setopt($ch, CURLOPT_HEADER, true); | |
curl_setopt($ch, CURLOPT_NOBODY, true); | |
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | |
curl_exec($ch); | |
$info = curl_getinfo($ch); | |
$contentLength = $info['download_content_length']; | |
shm_put_var($shm, 1, $contentLength); | |
// start the download | |
$written = -1; | |
$downloadFile($url, $destination, function ($ch, $bytes, $hnd) use ($shm, &$written) { | |
$length = fwrite($hnd, $bytes); | |
$written = $written ? $written + $length : $length; | |
shm_put_var($shm, 2, $written); | |
return $length; | |
}, $info); | |
shm_put_var($shm, 2, $written); | |
} catch (Exception $e) { | |
print_r($e); | |
} | |
shm_put_var($shm, 0, $info); // child finished | |
posix_kill($pid, SIGKILL); // kill the child | |
} | |
$last = -1; | |
while (($info = shm_get_var($shm, 0)) === null) { | |
if ($progressCallback) { | |
$total = shm_get_var($shm, 1); | |
$written = shm_get_var($shm, 2); | |
if ($written !== $last) { | |
call_user_func($progressCallback, $written, $total, $destination); | |
$last = $written; | |
} | |
} | |
usleep($interval * 1000); | |
} | |
$total = shm_get_var($shm, 1); | |
$written = shm_get_var($shm, 2); | |
shm_remove($shm); | |
if ($progressCallback) { | |
call_user_func($progressCallback, $written, $total, $destination); | |
} | |
} else { | |
// synchronous fallback if pcntl is not available or unable to allocate shared memory | |
$written = $downloadFile($url, $destination, null, $info); | |
} | |
return $written; | |
}; | |
$extract = function ($archive, $destination) { | |
$p = new PharData($archive); | |
$success = $p->extractTo($destination); | |
return $success; | |
}; | |
$do = function ($function, $args, $text, $error) { | |
fwrite(STDOUT, "$text\n"); | |
if (!call_user_func_array($function, $args)) { | |
fwrite(STDERR, "$error\n"); | |
exit(1); | |
} | |
}; | |
#endregion | |
$installedVersion = $getInstalledVersion() ?: 'n/a'; | |
fwrite(STDOUT, "Detecting installed version: $installedVersion\n"); | |
fwrite(STDOUT, "Detecting latest version: "); | |
$latestVersion = $getLatestVersion(); | |
fwrite(STDOUT, "$latestVersion\n"); | |
if (version_compare($latestVersion, $installedVersion) !== 1) { | |
fwrite(STDOUT, "No action required\n"); | |
exit(0); | |
} | |
fwrite(STDOUT, "Update required\n"); | |
$downloadName = "PhpStorm-$latestVersion.tar.gz"; | |
$downloadPath = getcwd() . "/$downloadName"; | |
if (!file_exists($downloadPath) || filesize($downloadPath) === 0) { | |
fwrite(STDOUT, "Finding the download link: "); | |
$downloadLink = $getDownloadLink(); | |
fwrite(STDOUT, "$downloadLink\n"); | |
fwrite(STDOUT, "Downloading target: '$downloadPath'\n"); | |
// download the file and display a progress indicator | |
$info = array(); | |
$downloadFileWithProgress($downloadLink, $downloadPath, function ($written, $total, $path) { | |
$pct = number_format(100 * ($written / $total), 2); | |
$written = number_format($written / 1024, 0); | |
$total = number_format($total / 1024, 0); | |
fwrite(STDOUT, "\r$written/$total kB [$pct%]"); | |
}, 100, $info); | |
$bytes = number_format($info['size_download'] / 1024, 0); | |
$time = number_format($info['total_time'], 2); | |
fwrite(STDOUT, "\rDownloaded $bytes kB in $time seconds\n"); | |
} else { | |
fwrite(STDOUT, "The file '$downloadName' exists skipping download\n"); | |
} | |
$temp = "$extractPath/" . substr($downloadName, 0, -7); | |
$dest = "$extractPath/$latestVersion"; | |
$link = "$extractPath/default"; | |
foreach (array($dest, $temp) as $path) { | |
if (is_dir($path) || !is_writable($extractPath)) { | |
fwrite(STDERR, "Cannot create folder '$path'\n"); | |
exit(1); | |
} | |
} | |
$do($extract, array($downloadPath, $extractPath), | |
"Extracting archive to '$temp'", | |
"Failed to extract the archive"); | |
$do('rename', array($temp, $dest), | |
"Renaming '$temp' -> '$dest'", | |
"Failed to rename the directory"); | |
if (is_link($link)) { | |
fwrite(STDOUT, "Removing existing symlink '$link'\n"); | |
unlink($link); | |
} | |
$do('symlink', array($dest, $link), | |
"Creating symlink '$link' -> '$dest'", | |
"Failed to create the symlink"); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment