mirror of
https://github.com/nextcloud/recognize.git
synced 2026-01-13 20:25:35 +00:00
319 lines
12 KiB
PHP
319 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
/**
|
|
* @copyright Copyright (c) 2020, Joas Schilling <coding@schilljs.com>
|
|
* @copyright Copyright (c) 2021, Marcel Klehr <mklehr@gmx.net>
|
|
*
|
|
* @author Joas Schilling <coding@schilljs.com>
|
|
*
|
|
* @license GNU AGPL version 3 or any later version
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
namespace OCA\Recognize\Migration;
|
|
|
|
use OCA\Recognize\Helper\TAR;
|
|
use OCP\AppFramework\Services\IAppConfig;
|
|
use OCP\Http\Client\IClientService;
|
|
use OCP\IBinaryFinder;
|
|
use OCP\Migration\IOutput;
|
|
use OCP\Migration\IRepairStep;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
final class InstallDeps implements IRepairStep {
|
|
public const NODE_VERSION = 'v20.9.0';
|
|
public const NODE_SERVER_OFFICIAL = 'https://nodejs.org/dist/';
|
|
public const NODE_SERVER_UNOFFICIAL = 'https://unofficial-builds.nodejs.org/download/release/';
|
|
|
|
protected IAppConfig $config;
|
|
private string $binaryDir;
|
|
private string $preGypBinaryDir;
|
|
private string $ffmpegDir;
|
|
private string $tfjsInstallScript;
|
|
private string $tfjsPath;
|
|
private IClientService $clientService;
|
|
private LoggerInterface $logger;
|
|
private string $tfjsGpuInstallScript;
|
|
private string $ffmpegInstallScript;
|
|
private string $tfjsGPUPath;
|
|
private IBinaryFinder $binaryFinder;
|
|
private string $nodeModulesDir;
|
|
|
|
public function __construct(IAppConfig $config, IClientService $clientService, LoggerInterface $logger, IBinaryFinder $binaryFinder) {
|
|
$this->config = $config;
|
|
$this->binaryDir = dirname(__DIR__, 2) . '/bin/';
|
|
$this->nodeModulesDir = dirname(__DIR__, 2) . '/node_modules/';
|
|
$this->preGypBinaryDir = dirname(__DIR__, 2) . '/node_modules/@mapbox/node-pre-gyp/bin/';
|
|
$this->ffmpegDir = dirname(__DIR__, 2) . '/node_modules/ffmpeg-static/';
|
|
$this->ffmpegInstallScript = dirname(__DIR__, 2) . '/node_modules/ffmpeg-static/install.js';
|
|
$this->tfjsInstallScript = dirname(__DIR__, 2) . '/node_modules/@tensorflow/tfjs-node/scripts/install.js';
|
|
$this->tfjsGpuInstallScript = dirname(__DIR__, 2) . '/node_modules/@tensorflow/tfjs-node-gpu/scripts/install.js';
|
|
$this->tfjsPath = dirname(__DIR__, 2) . '/node_modules/@tensorflow/tfjs-node/';
|
|
$this->tfjsGPUPath = dirname(__DIR__, 2) . '/node_modules/@tensorflow/tfjs-node-gpu/';
|
|
$this->clientService = $clientService;
|
|
$this->logger = $logger;
|
|
$this->binaryFinder = $binaryFinder;
|
|
}
|
|
|
|
public function getName(): string {
|
|
return 'Install recognize dependencies';
|
|
}
|
|
|
|
public function run(IOutput $output): void {
|
|
try {
|
|
$existingBinary = $this->config->getAppValueString('node_binary', '');
|
|
if ($existingBinary !== '') {
|
|
$version = $this->testBinary($existingBinary);
|
|
if ($version === null) {
|
|
$this->installNodeBinary($output);
|
|
}
|
|
} else {
|
|
$this->installNodeBinary($output);
|
|
}
|
|
|
|
$this->setBinariesPermissions();
|
|
|
|
$binaryPath = $this->config->getAppValueString('node_binary', '');
|
|
|
|
$this->runTfjsInstall($binaryPath);
|
|
$this->runFfmpegInstall($binaryPath);
|
|
$this->runTfjsGpuInstall($binaryPath);
|
|
$this->setNodeModulesPermissions();
|
|
$this->setNiceBinaryPath();
|
|
$this->setFfmpegBinaryPath();
|
|
} catch (\Throwable $e) {
|
|
$output->warning('Failed to automatically install dependencies for recognize. Check the recognize admin panel for potential problems.');
|
|
$this->logger->error('Failed to automatically install dependencies for recognize. Check the recognize admin panel for potential problems.', ['exception' => $e]);
|
|
}
|
|
}
|
|
|
|
protected function setNiceBinaryPath() : void {
|
|
/* use nice binary from settings if available */
|
|
if ($this->config->getAppValueString('nice_binary', '') !== '') {
|
|
$nice_path = $this->config->getAppValueString('nice_binary');
|
|
} else {
|
|
/* returns the path to the nice binary or false if not found */
|
|
$nice_path = $this->binaryFinder->findBinaryPath('nice');
|
|
}
|
|
|
|
if ($nice_path !== false) {
|
|
$this->config->setAppValueString('nice_binary', $nice_path);
|
|
} else {
|
|
$this->config->setAppValueString('nice_binary', '');
|
|
}
|
|
}
|
|
|
|
protected function installNodeBinary(IOutput $output) : void {
|
|
$isARM = false;
|
|
$isMusl = false;
|
|
$uname = php_uname('m');
|
|
|
|
if ($uname === 'x86_64') {
|
|
$binaryPath = $this->downloadNodeBinary(self::NODE_SERVER_OFFICIAL, self::NODE_VERSION, 'x64');
|
|
$version = $this->testBinary($binaryPath);
|
|
|
|
if ($version === null) {
|
|
$binaryPath = $this->downloadNodeBinary(self::NODE_SERVER_UNOFFICIAL, self::NODE_VERSION, 'x64', 'musl');
|
|
$version = $this->testBinary($binaryPath);
|
|
if ($version !== null) {
|
|
$isMusl = true;
|
|
}
|
|
}
|
|
} elseif ($uname === 'aarch64') {
|
|
$binaryPath = $this->downloadNodeBinary(self::NODE_SERVER_OFFICIAL, self::NODE_VERSION, 'arm64');
|
|
$version = $this->testBinary($binaryPath);
|
|
if ($version !== null) {
|
|
$isARM = true;
|
|
}
|
|
} elseif ($uname === 'armv7l') {
|
|
$binaryPath = $this->downloadNodeBinary(self::NODE_SERVER_OFFICIAL, self::NODE_VERSION, 'armv7l');
|
|
$version = $this->testBinary($binaryPath);
|
|
if ($version !== null) {
|
|
$isARM = true;
|
|
}
|
|
} else {
|
|
$output->warning('CPU architecture $uname is not supported.');
|
|
return;
|
|
}
|
|
|
|
if ($version === null) {
|
|
$output->warning('Failed to install node binary');
|
|
return;
|
|
}
|
|
|
|
// Write the app config
|
|
$this->config->setAppValueString('node_binary', $binaryPath);
|
|
|
|
$supportsAVX = $this->isAVXSupported();
|
|
if ($isARM || $isMusl || !$supportsAVX) {
|
|
$output->info('Enabling purejs mode (isMusl='.$isMusl.', isARM='.$isARM.', supportsAVX='.$supportsAVX.')');
|
|
$this->config->setAppValueString('tensorflow.purejs', 'true');
|
|
}
|
|
}
|
|
|
|
protected function testBinary(string $binaryPath): ?string {
|
|
if (!file_exists($binaryPath)) {
|
|
return null;
|
|
}
|
|
try {
|
|
// Make binary executable
|
|
chmod($binaryPath, 0755);
|
|
|
|
$cmd = escapeshellcmd($binaryPath) . ' ' . escapeshellarg('--version');
|
|
|
|
exec($cmd . ' 2>&1', $output, $returnCode);
|
|
} catch (\Throwable $e) {
|
|
}
|
|
|
|
if ($returnCode !== 0) {
|
|
return null;
|
|
}
|
|
|
|
return trim(implode("\n", $output));
|
|
}
|
|
|
|
protected function runTfjsInstall(string $nodeBinary) : void {
|
|
$oriCwd = getcwd();
|
|
chdir($this->tfjsPath);
|
|
$cmd = 'PATH='.escapeshellcmd($this->preGypBinaryDir).':'.escapeshellcmd($this->binaryDir).':$PATH ' . escapeshellcmd($nodeBinary) . ' ' . escapeshellarg($this->tfjsInstallScript) . ' cpu ' . escapeshellarg('download');
|
|
try {
|
|
exec($cmd . ' 2>&1', $output, $returnCode); // Appending 2>&1 to avoid leaking sterr
|
|
} catch (\Throwable $e) {
|
|
$this->logger->error('Failed to install Tensorflow.js: '.$e->getMessage(), ['exception' => $e]);
|
|
throw new \Exception('Failed to install Tensorflow.js: '.$e->getMessage());
|
|
}
|
|
chdir($oriCwd);
|
|
if ($returnCode !== 0) {
|
|
$this->logger->error('Failed to install Tensorflow.js: '.trim(implode("\n", $output)));
|
|
throw new \Exception('Failed to install Tensorflow.js: '.trim(implode("\n", $output)));
|
|
}
|
|
}
|
|
|
|
protected function runTfjsGpuInstall(string $nodeBinary) : void {
|
|
$oriCwd = getcwd();
|
|
chdir($this->tfjsGPUPath);
|
|
$cmd = 'PATH='.escapeshellcmd($this->preGypBinaryDir).':'.escapeshellcmd($this->binaryDir).':$PATH ' . escapeshellcmd($nodeBinary) . ' ' . escapeshellarg($this->tfjsGpuInstallScript) . ' gpu ' . escapeshellarg('download');
|
|
try {
|
|
exec($cmd . ' 2>&1', $output, $returnCode); // Appending 2>&1 to avoid leaking sterr
|
|
} catch (\Throwable $e) {
|
|
$this->logger->error('Failed to install Tensorflow.js for GPU: '.$e->getMessage(), ['exception' => $e]);
|
|
throw new \Exception('Failed to install Tensorflow.js for GPU: '.$e->getMessage());
|
|
}
|
|
chdir($oriCwd);
|
|
if ($returnCode !== 0) {
|
|
$this->logger->error('Failed to install Tensorflow.js for GPU: '.trim(implode("\n", $output)));
|
|
throw new \Exception('Failed to install Tensorflow.js for GPU: '.trim(implode("\n", $output)));
|
|
}
|
|
}
|
|
|
|
protected function runFfmpegInstall(string $nodeBinary): void {
|
|
$oriCwd = getcwd();
|
|
chdir($this->ffmpegDir);
|
|
$cmd = escapeshellcmd($nodeBinary) . ' ' . escapeshellarg($this->ffmpegInstallScript);
|
|
try {
|
|
exec($cmd . ' 2>&1', $output, $returnCode); // Appending 2>&1 to avoid leaking sterr
|
|
} catch (\Throwable $e) {
|
|
$this->logger->error('Failed to install ffmpeg: '.$e->getMessage(), ['exception' => $e]);
|
|
throw new \Exception('Failed to install ffmpeg: '.$e->getMessage());
|
|
}
|
|
chdir($oriCwd);
|
|
if ($returnCode !== 0) {
|
|
$this->logger->error('Failed to install ffmpeg: '.trim(implode("\n", $output)));
|
|
throw new \Exception('Failed to install ffmpeg: '.trim(implode("\n", $output)));
|
|
}
|
|
}
|
|
|
|
protected function setFfmpegBinaryPath() : void {
|
|
/* use nice binary from settings if available */
|
|
if ($this->config->getAppValueString('ffmpeg_binary', '') !== '') {
|
|
$ffmpeg_path = $this->config->getAppValueString('ffmpeg_binary');
|
|
} else {
|
|
/* returns the path to the nice binary or false if not found */
|
|
$ffmpeg_path = $this->binaryFinder->findBinaryPath('ffmpeg');
|
|
}
|
|
|
|
if ($ffmpeg_path !== false) {
|
|
$this->config->setAppValueString('ffmpeg_binary', $ffmpeg_path);
|
|
} else {
|
|
$this->config->setAppValueString('ffmpeg_binary', __DIR__ . '/../../node_modules/ffmpeg-static/ffmpeg');
|
|
}
|
|
}
|
|
|
|
protected function downloadNodeBinary(string $server, string $version, string $arch, string $flavor = '') : string {
|
|
$name = 'node-'.$version.'-linux-'.$arch;
|
|
if ($flavor !== '') {
|
|
$name = $name . '-'.$flavor;
|
|
}
|
|
$url = $server.$version.'/'.$name.'.tar.gz';
|
|
$file = $this->binaryDir.$arch.'.tar.gz';
|
|
try {
|
|
$this->clientService->newClient()->get($url, ['timeout' => 60 * 20, 'sink' => $file]);
|
|
} catch (\Exception $e) {
|
|
$this->logger->error('Downloading of node binary failed', ['exception' => $e]);
|
|
throw new \Exception('Downloading of node binary failed');
|
|
}
|
|
$tar = new TAR($file);
|
|
$tar->extractFile($name.'/bin/node', $this->binaryDir.'node');
|
|
return $this->binaryDir.'node';
|
|
}
|
|
|
|
/**
|
|
* try to fix binaries permissions issues
|
|
*/
|
|
protected function setBinariesPermissions(): void {
|
|
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->preGypBinaryDir));
|
|
foreach ($iterator as $item) {
|
|
if (chmod(realpath($item->getPathname()), 0755) === false) {
|
|
throw new \Exception('Error when setting '.$this->preGypBinaryDir.'* permissions');
|
|
}
|
|
}
|
|
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->ffmpegDir));
|
|
foreach ($iterator as $item) {
|
|
if (chmod(realpath($item->getPathname()), 0755) === false) {
|
|
throw new \Exception('Error when setting '.$this->ffmpegDir.'* permissions');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set write permissions recursively for node_modules
|
|
*/
|
|
protected function setNodeModulesPermissions(): void {
|
|
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->nodeModulesDir));
|
|
/** @var \SplFileInfo $item */
|
|
foreach ($iterator as $item) {
|
|
$realpath = realpath($item->getPathname());
|
|
if ($realpath === false || ($item->getPerms() & 0700) === 0700) {
|
|
continue;
|
|
}
|
|
$mode = $item->isDir() ? 0755 : 0644;
|
|
if (chmod($realpath, $mode) === false) {
|
|
throw new \Exception('Error when setting '.$this->nodeModulesDir.'* permissions: ' . $realpath);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function isAVXSupported(): bool {
|
|
try {
|
|
$cpuinfo = file_get_contents('/proc/cpuinfo');
|
|
} catch (\Throwable $e) {
|
|
return false;
|
|
}
|
|
|
|
return $cpuinfo !== false && strpos($cpuinfo, 'avx') !== false;
|
|
}
|
|
}
|