Prevent duplicate logs being added for torrent

This commit is contained in:
itismadness
2025-09-05 21:30:42 +00:00
committed by Spine
parent 908155ae62
commit 2de542715b
11 changed files with 180 additions and 9 deletions

View File

@@ -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;
}
}

View File

@@ -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<Logfile>
*/
public function all(): array {
return $this->list;
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Gazelle\Manager;
use Gazelle\Logfile as Logfile;
@@ -21,7 +23,7 @@ class TorrentLog extends \Gazelle\Base {
$logId = self::$db->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 {

View File

@@ -818,4 +818,20 @@ abstract class TorrentAbstract extends BaseAttrObject {
}
return '';
}
/**
* Get array of the hashes of all logfiles for a torrent
* @return array<string>
*/
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;
}
}

View File

@@ -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();

View File

@@ -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);

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Gazelle;
use PHPUnit\Framework\TestCase;
class LogfileSummaryTest extends TestCase {
public function testSummary(): void {
$logfileSummary = new LogfileSummary([
'error' => [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');
}
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Gazelle;
use GazelleUnitTest\Helper;
use PHPUnit\Framework\TestCase;
use OrpheusNET\Logchecker\Logchecker;
class TorrentLogTest extends TestCase {
public function testFindById(): void {
$user = null;
$tgroup = null;
try {
$user = Helper::makeUser('torrentlog.' . randomString(6), 'torrentlog');
$user->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));
}
}