diff --git a/app/File.php b/app/File.php index 9dad5c1f4..2031c0ae8 100644 --- a/app/File.php +++ b/app/File.php @@ -45,4 +45,15 @@ abstract class File extends BaseObject { public function remove(): int { return (int)unlink($this->path()); } + + /** + * Get the hash of the file + */ + public function hash(): string { + $hash = hash_file(DIGEST_ALGO, $this->path()); + if (!$hash) { + throw new \Exception("Failed to compute hash for file: {$this->path()}"); + } + return $hash; + } } diff --git a/app/LogfileSummary.php b/app/LogfileSummary.php index a086df7bf..7e99492ed 100644 --- a/app/LogfileSummary.php +++ b/app/LogfileSummary.php @@ -7,10 +7,15 @@ class LogfileSummary { protected bool $allChecksum = true; protected int $lowestScore = 100; - public function __construct(array $fileList = []) { + public function __construct(array $fileList = [], array $hashes = []) { $this->list = []; for ($n = 0, $end = count($fileList['error']); $n < $end; ++$n) { - if ($fileList['error'][$n] == UPLOAD_ERR_OK) { + if ($fileList['error'][$n] === UPLOAD_ERR_OK) { + $hash = hash_file(DIGEST_ALGO, $fileList['tmp_name'][$n]); + if (in_array($hash, $hashes)) { + continue; + } + $hashes[] = $hash; $log = new Logfile($fileList['tmp_name'][$n], $fileList['name'][$n]); $this->allChecksum = $this->allChecksum && $log->checksum(); $this->lowestScore = min($this->lowestScore, $log->score()); @@ -31,6 +36,9 @@ class LogfileSummary { return $this->lowestScore; } + /** + * @return array + */ public function all(): array { return $this->list; } diff --git a/app/Manager/TorrentLog.php b/app/Manager/TorrentLog.php index 4fa2e004f..7ce587c17 100644 --- a/app/Manager/TorrentLog.php +++ b/app/Manager/TorrentLog.php @@ -1,5 +1,7 @@ inserted_id(); new RipLog($torrent->id, $logId)->put($logfile->filepath()); new RipLogHTML($torrent->id, $logId)->put($logfile->text()); - return $this->findById($torrent, $logId); + return new \Gazelle\TorrentLog($torrent, $logId); } public function findById(Torrent $torrent, int $id): ?\Gazelle\TorrentLog { diff --git a/app/TorrentAbstract.php b/app/TorrentAbstract.php index a57fa825d..5ecb6604f 100644 --- a/app/TorrentAbstract.php +++ b/app/TorrentAbstract.php @@ -818,4 +818,20 @@ abstract class TorrentAbstract extends BaseAttrObject { } return ''; } + + /** + * Get array of the hashes of all logfiles for a torrent + * @return array + */ + public function logfileHashList(): array { + self::$db->prepared_query(" + SELECT LogID FROM torrents_logs WHERE TorrentID = ? + ", $this->id + ); + $hashes = []; + foreach (self::$db->collect(0) as $logId) { + $hashes[] = new RipLog($this->id, $logId)->hash(); + } + return $hashes; + } } diff --git a/sections/ajax/add_log.php b/sections/ajax/add_log.php index 020fbc8f2..5d9bd0364 100644 --- a/sections/ajax/add_log.php +++ b/sections/ajax/add_log.php @@ -16,11 +16,13 @@ if (empty($_FILES) || empty($_FILES['logfiles'])) { json_error('no log files uploaded'); } +$manager = new Manager\TorrentLog(); + echo new Json\AddLog( $torrent, $Viewer, - new Manager\TorrentLog(), - new LogfileSummary($_FILES['logfiles']), + $manager, + new LogfileSummary($_FILES['logfiles'], $torrent->logfileHashList()), ) ->setVersion(1) ->response(); diff --git a/sections/logchecker/upload_handle.php b/sections/logchecker/upload_handle.php index 0885d4a6b..c122b4c9a 100644 --- a/sections/logchecker/upload_handle.php +++ b/sections/logchecker/upload_handle.php @@ -22,16 +22,16 @@ if ($torrent->uploaderId() != $Viewer->id && !$Viewer->permitted('admin_add_log' } $action = in_array($_POST['from_action'], ['upload', 'update']) ? $_POST['from_action'] : 'upload'; -$logfileSummary = new LogfileSummary($_FILES['logfiles']); +$logfileSummary = new LogfileSummary($_FILES['logfiles'], $torrent->logfileHashList()); if (!$logfileSummary->total()) { - Error400::error("No logfiles uploaded."); + Error400::error("No (new) logfiles uploaded."); } else { $torrent->removeLogDb(); new File\RipLog($torrent->id, '*')->remove(); new File\RipLogHTML($torrent->id, '*')->remove(); - $torrentLogManager = new Manager\TorrentLog(); + $torrentLogManager = new Manager\TorrentLog(); $checkerVersion = Logchecker::getLogcheckerVersion(); foreach ($logfileSummary->all() as $logfile) { $torrentLogManager->create($torrent, $logfile, $checkerVersion); diff --git a/sections/torrents/edit_handle.php b/sections/torrents/edit_handle.php index e07282298..3d7c99ee4 100644 --- a/sections/torrents/edit_handle.php +++ b/sections/torrents/edit_handle.php @@ -207,7 +207,7 @@ $db = DB::DB(); $db->begin_transaction(); // It's all or nothing if (isset($_FILES['logfiles'])) { - $logfileSummary = new LogfileSummary($_FILES['logfiles']); + $logfileSummary = new LogfileSummary($_FILES['logfiles'], $torrent->logfileHashList()); if ($logfileSummary->total()) { $torrentLogManager = new Manager\TorrentLog(); $checkerVersion = Logchecker::getLogcheckerVersion(); diff --git a/tests/phpunit/FileStorageTest.php b/tests/phpunit/FileStorageTest.php index f73933678..728ea8238 100644 --- a/tests/phpunit/FileStorageTest.php +++ b/tests/phpunit/FileStorageTest.php @@ -144,4 +144,10 @@ class FileStorageTest extends TestCase { $this->assertEquals('', $file->link(), 'file-t-link'); $this->assertEquals('', $file->location(), 'file-t-location'); } + + public function testFileHashFailure(): void { + $file = new File\Torrent(906623); + $this->expectException(\Exception::class); + $file->hash(); + } } diff --git a/tests/phpunit/LogfileSummaryTest.php b/tests/phpunit/LogfileSummaryTest.php new file mode 100644 index 000000000..a6a500917 --- /dev/null +++ b/tests/phpunit/LogfileSummaryTest.php @@ -0,0 +1,46 @@ + [UPLOAD_ERR_OK], + 'name' => ['valid_log_eac.log'], + 'tmp_name' => [__DIR__ . '/../fixture/valid_log_eac.log'], + ]); + + $this->assertTrue($logfileSummary->checksum(), 'logfilesummary-checksum'); + $this->assertEquals('1', $logfileSummary->checksumStatus(), 'logfilesummary-checksum-status'); + $this->assertEquals(100, $logfileSummary->overallScore(), 'logfilesummary-overall-score'); + $this->assertCount(1, $logfileSummary->all(), 'logfilesummary-all'); + $this->assertEquals(1, $logfileSummary->total(), 'logfilesummary-total'); + } + + public function testDuplicateFilesIgnored(): void { + $logfileSummary = new LogfileSummary([ + 'error' => [UPLOAD_ERR_OK, UPLOAD_ERR_OK], + 'name' => ['valid_log_eac.log', 'valid_log_eac.log'], + 'tmp_name' => [__DIR__ . '/../fixture/valid_log_eac.log', __DIR__ . '/../fixture/valid_log_eac.log'], + ]); + + $this->assertTrue($logfileSummary->checksum(), 'logfilesummary-checksum'); + $this->assertEquals('1', $logfileSummary->checksumStatus(), 'logfilesummary-checksum-status'); + $this->assertEquals(100, $logfileSummary->overallScore(), 'logfilesummary-overall-score'); + $this->assertCount(1, $logfileSummary->all(), 'logfilesummary-all'); + $this->assertEquals(1, $logfileSummary->total(), 'logfilesummary-total'); + } + + public function testDuplicateHashesIgnored(): void { + $logfileSummary = new LogfileSummary([ + 'error' => [UPLOAD_ERR_OK], + 'name' => ['valid_log_eac.log'], + 'tmp_name' => [__DIR__ . '/../fixture/valid_log_eac.log'], + ], [hash_file(DIGEST_ALGO, __DIR__ . '/../fixture/valid_log_eac.log')]); + + $this->assertCount(0, $logfileSummary->all(), 'logfilesummary-all'); + $this->assertEquals(0, $logfileSummary->total(), 'logfilesummary-total'); + } +} diff --git a/tests/phpunit/TorrentTest.php b/tests/phpunit/TorrentTest.php index df4c8d890..8b3847558 100644 --- a/tests/phpunit/TorrentTest.php +++ b/tests/phpunit/TorrentTest.php @@ -7,6 +7,7 @@ use GazelleUnitTest\Helper; use Gazelle\Enum\DownloadStatus; use Gazelle\Enum\TorrentFlag; use Gazelle\Enum\UserTorrentSearch; +use OrpheusNET\Logchecker\Logchecker; class TorrentTest extends TestCase { protected Torrent $torrent; @@ -365,4 +366,26 @@ class TorrentTest extends TestCase { "collector-tlist-summary", ); } + + public function testLogfileHashList(): void { + try { + $logfileSummary = new LogfileSummary([ + 'error' => [UPLOAD_ERR_OK], + 'name' => ['valid_log_eac.log'], + 'tmp_name' => [__DIR__ . '/../fixture/valid_log_eac.log'], + ]); + $torrentLogManager = new Manager\TorrentLog(); + $checkerVersion = Logchecker::getLogcheckerVersion(); + foreach ($logfileSummary->all() as $logfile) { + $torrentLog = $torrentLogManager->create($this->torrent, $logfile, $checkerVersion); + // Because RipLog::put relies on move_uploaded_file, the create method above fails to put the log file + // into place, so we do this copy afterwards. + $ripLog = new File\RipLog($torrentLog->torrentId(), $torrentLog->id()); + copy(__DIR__ . '/../fixture/valid_log_eac.log', $ripLog->path()); + } + $this->assertEquals([hash_file('sha256', __DIR__ . '/../fixture/valid_log_eac.log')], $this->torrent->logfileHashList()); + } finally { + new File\RipLog($this->torrent->id, '*')->remove(); + } + } } diff --git a/tests/phpunit/manager/TorrentLogTest.php b/tests/phpunit/manager/TorrentLogTest.php new file mode 100644 index 000000000..6af9e2ac5 --- /dev/null +++ b/tests/phpunit/manager/TorrentLogTest.php @@ -0,0 +1,57 @@ +requestContext()->setViewer($user); + $tgroup = Helper::makeTGroupMusic( + $user, + 'phpunit torrentlog ' . randomString(6), + [[ARTIST_MAIN], ['phpunit torrentlog artist ' . randomString(6)]], + ['czech'] + ); + $torrent = Helper::makeTorrentMusic( + tgroup: $tgroup, + user: $user, + title: randomString(10), + ); + $logfileSummary = new LogfileSummary([ + 'error' => [UPLOAD_ERR_OK], + 'name' => ['valid_log_eac.log'], + 'tmp_name' => [__DIR__ . '/../fixture/valid_log_eac.log'], + ]); + + $torrentLogManager = new Manager\TorrentLog(); + $checkerVersion = Logchecker::getLogcheckerVersion(); + $torrentLog = null; + foreach ($logfileSummary->all() as $logfile) { + $torrentLog = $torrentLogManager->create($torrent, $logfile, $checkerVersion); + } + $torrentLog2 = $torrentLogManager->findById($torrent, $torrentLog->id()); + $this->assertInstanceOf(TorrentLog::class, $torrentLog2); + $this->assertEquals($torrentLog->id(), $torrentLog2->id()); + $this->assertEquals($torrentLog->link(), $torrentLog2->link()); + } finally { + if (isset($tgroup)) { + Helper::removeTGroup($tgroup, $user); + } + if (isset($user)) { + $user->remove(); + } + } + } + + public function testFindByIdNull(): void { + $manager = new Manager\TorrentLog(); + $this->assertNull($manager->findById(new Torrent(1), 1)); + } +}