mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 17:01:16 +00:00
Update cbp and add FKs back
This commit is contained in:
@@ -11,6 +11,8 @@ use Illuminate\Support\Str;
|
||||
|
||||
class CollectionCleanupService
|
||||
{
|
||||
private const MAX_SQL_ROWS_PER_STATEMENT = 500;
|
||||
|
||||
/**
|
||||
* Maximum number of retries for a lock-related DB error before giving up.
|
||||
*/
|
||||
@@ -50,28 +52,27 @@ class CollectionCleanupService
|
||||
), true);
|
||||
}
|
||||
|
||||
// Batch-delete old collections using a safe id-subselect to avoid read-then-delete races.
|
||||
// The DELETE here is single-table (no JOIN), so cross-table deadlocks
|
||||
// are not a concern; the helper still gives us bounded retries with
|
||||
// jittered backoff and re-throws non-lock errors instead of swallowing them.
|
||||
// Batch-delete old collections using select-then-delete so we can
|
||||
// explicitly remove parts/binaries/collections even when FK cascades
|
||||
// are not present in the runtime schema.
|
||||
$cutoff = now()->subHours(Settings::settingValue('partretentionhours'));
|
||||
$batchDeleted = 0;
|
||||
do {
|
||||
$affected = $this->retryOnLockError(
|
||||
fn (): int => DB::affectingStatement(
|
||||
'DELETE FROM collections WHERE id IN (
|
||||
SELECT id FROM (
|
||||
SELECT id FROM collections WHERE dateadded < ? ORDER BY id LIMIT 500
|
||||
) AS x
|
||||
)',
|
||||
[$cutoff]
|
||||
),
|
||||
'Cleanup',
|
||||
$echoCLI,
|
||||
);
|
||||
$ids = DB::table('collections')
|
||||
->where('dateadded', '<', $cutoff)
|
||||
->orderBy('id')
|
||||
->limit(self::MAX_SQL_ROWS_PER_STATEMENT)
|
||||
->pluck('id')
|
||||
->all();
|
||||
|
||||
if ($ids === []) {
|
||||
break;
|
||||
}
|
||||
|
||||
$affected = $this->deleteCollectionsAndDescendants($ids, 'Cleanup', $echoCLI);
|
||||
|
||||
$batchDeleted += $affected;
|
||||
if ($affected < 500) {
|
||||
if ($affected < self::MAX_SQL_ROWS_PER_STATEMENT) {
|
||||
break;
|
||||
}
|
||||
// Brief pause to reduce pressure on the lock manager in busy systems.
|
||||
@@ -125,7 +126,7 @@ class CollectionCleanupService
|
||||
if ($echoCLI) {
|
||||
cli()->primary(
|
||||
'Finished deleting '.$missedDeleted.' collections missed after NZB creation in '.($totalTime).Str::plural(' second', (int) $totalTime).
|
||||
PHP_EOL.'Removed '.number_format($deletedCount).' parts/binaries/collection rows in '.$totalTime.Str::plural(' second', (int) $totalTime),
|
||||
PHP_EOL.'Removed '.number_format($deletedCount).' collections (with related binaries/parts) in '.$totalTime.Str::plural(' second', (int) $totalTime),
|
||||
true
|
||||
);
|
||||
}
|
||||
@@ -146,7 +147,7 @@ class CollectionCleanupService
|
||||
{
|
||||
$deleted = 0;
|
||||
$maxBatches = 20; // hard cap per cycle; bounded backlog drain
|
||||
$batchSize = 500;
|
||||
$batchSize = self::MAX_SQL_ROWS_PER_STATEMENT;
|
||||
|
||||
for ($i = 0; $i < $maxBatches; $i++) {
|
||||
$ids = DB::table('collections as c')
|
||||
@@ -162,11 +163,7 @@ class CollectionCleanupService
|
||||
break;
|
||||
}
|
||||
|
||||
$affected = $this->retryOnLockError(
|
||||
fn (): int => DB::table('collections')->whereIn('id', $ids)->delete(),
|
||||
'Orphan cleanup',
|
||||
$echoCLI,
|
||||
);
|
||||
$affected = $this->deleteCollectionsAndDescendants($ids, 'Orphan cleanup', $echoCLI);
|
||||
|
||||
$deleted += $affected;
|
||||
if ($affected < $batchSize) {
|
||||
@@ -199,7 +196,7 @@ class CollectionCleanupService
|
||||
{
|
||||
$deleted = 0;
|
||||
$maxBatches = 20;
|
||||
$batchSize = 500;
|
||||
$batchSize = self::MAX_SQL_ROWS_PER_STATEMENT;
|
||||
|
||||
for ($i = 0; $i < $maxBatches; $i++) {
|
||||
$ids = DB::table('collections as c')
|
||||
@@ -214,11 +211,7 @@ class CollectionCleanupService
|
||||
break;
|
||||
}
|
||||
|
||||
$affected = $this->retryOnLockError(
|
||||
fn (): int => DB::table('collections')->whereIn('id', $ids)->delete(),
|
||||
'Missed-NZB cleanup',
|
||||
$echoCLI,
|
||||
);
|
||||
$affected = $this->deleteCollectionsAndDescendants($ids, 'Missed-NZB cleanup', $echoCLI);
|
||||
|
||||
$deleted += $affected;
|
||||
if ($affected < $batchSize) {
|
||||
@@ -230,6 +223,51 @@ class CollectionCleanupService
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly delete parts, binaries, then collections for the given IDs.
|
||||
* This path does not rely on DB-level cascade constraints.
|
||||
*
|
||||
* @param list<int> $collectionIds
|
||||
*/
|
||||
public function deleteCollectionsAndDescendants(
|
||||
array $collectionIds,
|
||||
string $label = 'CBP cleanup',
|
||||
bool $echoCLI = false,
|
||||
): int {
|
||||
if ($collectionIds === []) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$deletedCollections = 0;
|
||||
|
||||
foreach (array_chunk($collectionIds, self::MAX_SQL_ROWS_PER_STATEMENT) as $chunk) {
|
||||
$placeholders = implode(',', array_fill(0, count($chunk), '?'));
|
||||
$deletedCollections += $this->retryOnLockError(
|
||||
fn (): int => DB::transaction(
|
||||
function () use ($chunk, $placeholders): int {
|
||||
DB::statement(
|
||||
"DELETE FROM parts WHERE binaries_id IN (SELECT id FROM binaries WHERE collections_id IN ({$placeholders}))",
|
||||
$chunk
|
||||
);
|
||||
DB::statement(
|
||||
"DELETE FROM binaries WHERE collections_id IN ({$placeholders})",
|
||||
$chunk
|
||||
);
|
||||
|
||||
return (int) DB::affectingStatement(
|
||||
"DELETE FROM collections WHERE id IN ({$placeholders})",
|
||||
$chunk
|
||||
);
|
||||
}
|
||||
),
|
||||
$label,
|
||||
$echoCLI,
|
||||
);
|
||||
}
|
||||
|
||||
return $deletedCollections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a DB write inside a bounded retry loop that only swallows transient
|
||||
* InnoDB lock errors (deadlock 1213, lock wait timeout 1205). Any other
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Models\Collection;
|
||||
use App\Models\Part;
|
||||
use App\Models\Release;
|
||||
use App\Models\Settings;
|
||||
use App\Services\CollectionCleanupService;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
@@ -57,8 +58,9 @@ class NzbService
|
||||
|
||||
protected string $siteCommentString;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CollectionCleanupService $collectionCleanupService,
|
||||
) {
|
||||
try {
|
||||
$nzbSplitLevel = (int) Settings::settingValue('nzbsplitlevel');
|
||||
} catch (QueryException $e) {
|
||||
@@ -217,9 +219,11 @@ class NzbService
|
||||
// Delete CBP (Collections, Binaries, Parts) for release that has its NZB created.
|
||||
// Use a transaction to ensure cascading deletes complete properly.
|
||||
try {
|
||||
DB::transaction(function () use ($release) {
|
||||
Collection::query()->where('releases_id', $release->id)->delete();
|
||||
});
|
||||
$this->collectionCleanupService->deleteCollectionsAndDescendants(
|
||||
$collectionIds,
|
||||
'NZB cleanup',
|
||||
false
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
// Log the error but don't fail the NZB creation since the file was written successfully
|
||||
Log::warning('Failed to delete collections for release '.$release->id.': '.$e->getMessage());
|
||||
|
||||
@@ -21,7 +21,8 @@ use Illuminate\Support\Str;
|
||||
class ReleaseCreationService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ReleaseCleaningService $releaseCleaning
|
||||
private readonly ReleaseCleaningService $releaseCleaning,
|
||||
private readonly CollectionCleanupService $collectionCleanupService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -171,9 +172,11 @@ class ReleaseCreationService
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DB::transaction(static function () use ($collection) {
|
||||
Collection::query()->where('collectionhash', $collection->collectionhash)->delete();
|
||||
}, 10);
|
||||
$this->collectionCleanupService->deleteCollectionsAndDescendants(
|
||||
[$collection->id],
|
||||
'Duplicate cleanup',
|
||||
$echoCLI
|
||||
);
|
||||
|
||||
$duplicate++;
|
||||
}
|
||||
|
||||
@@ -85,11 +85,11 @@ final class ReleaseProcessingService
|
||||
$this->releaseCleaning = $releaseCleaning ?? new ReleaseCleaningService;
|
||||
$this->releaseManagement = $releaseManagement ?? app(ReleaseManagementService::class);
|
||||
$this->releaseImage = $releaseImage ?? new ReleaseImageService;
|
||||
|
||||
$this->releaseCreationService = $releaseCreationService
|
||||
?? new ReleaseCreationService($this->releaseCleaning);
|
||||
$this->collectionCleanupService = $collectionCleanupService
|
||||
?? new CollectionCleanupService;
|
||||
|
||||
$this->releaseCreationService = $releaseCreationService
|
||||
?? new ReleaseCreationService($this->releaseCleaning, $this->collectionCleanupService);
|
||||
$this->postProcessService = $postProcessService;
|
||||
|
||||
$this->settings = $this->loadSettings();
|
||||
@@ -432,21 +432,22 @@ final class ReleaseProcessingService
|
||||
$stats = ['minSize' => 0, 'maxSize' => 0, 'minFiles' => 0, 'par2Only' => 0];
|
||||
|
||||
// Delete collections where ALL binaries are par2 files (no actual content)
|
||||
DB::transaction(static function () use (&$stats): void {
|
||||
$par2OnlyCollectionIds = DB::table('collections as c')
|
||||
->join('binaries as b', 'c.id', '=', 'b.collections_id')
|
||||
->where('c.filecheck', CollectionFileCheckStatus::Sized->value)
|
||||
->where('c.filesize', '>', 0)
|
||||
->groupBy('c.id')
|
||||
->havingRaw("COUNT(b.id) = SUM(CASE WHEN b.name REGEXP '\\\\.(vol[0-9]+\\\\+[0-9]+\\\\.par2|par2)' THEN 1 ELSE 0 END)")
|
||||
->pluck('c.id');
|
||||
$par2OnlyCollectionIds = DB::table('collections as c')
|
||||
->join('binaries as b', 'c.id', '=', 'b.collections_id')
|
||||
->where('c.filecheck', CollectionFileCheckStatus::Sized->value)
|
||||
->where('c.filesize', '>', 0)
|
||||
->groupBy('c.id')
|
||||
->havingRaw("COUNT(b.id) = SUM(CASE WHEN b.name REGEXP '\\\\.(vol[0-9]+\\\\+[0-9]+\\\\.par2|par2)' THEN 1 ELSE 0 END)")
|
||||
->pluck('c.id')
|
||||
->all();
|
||||
|
||||
if ($par2OnlyCollectionIds->isNotEmpty()) {
|
||||
$stats['par2Only'] += Collection::query()
|
||||
->whereIn('id', $par2OnlyCollectionIds)
|
||||
->delete();
|
||||
}
|
||||
}, 10);
|
||||
if ($par2OnlyCollectionIds !== []) {
|
||||
$stats['par2Only'] += $this->collectionCleanupService->deleteCollectionsAndDescendants(
|
||||
$par2OnlyCollectionIds,
|
||||
'Par2-only cleanup',
|
||||
$this->echoCLI
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($groupIDs as $grpID) {
|
||||
$groupSettings = UsenetGroup::getGroupByID($grpID['id']);
|
||||
@@ -457,32 +458,48 @@ final class ReleaseProcessingService
|
||||
continue;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($groupMinSize, $groupMinFiles, &$stats): void {
|
||||
$effectiveMinSize = max($groupMinSize, $this->settings->minSizeToFormRelease);
|
||||
if ($effectiveMinSize > 0) {
|
||||
$stats['minSize'] += Collection::query()
|
||||
->where('filecheck', CollectionFileCheckStatus::Sized->value)
|
||||
->where('filesize', '>', 0)
|
||||
->where('filesize', '<', $effectiveMinSize)
|
||||
->delete();
|
||||
}
|
||||
$effectiveMinSize = max($groupMinSize, $this->settings->minSizeToFormRelease);
|
||||
if ($effectiveMinSize > 0) {
|
||||
$ids = Collection::query()
|
||||
->where('filecheck', CollectionFileCheckStatus::Sized->value)
|
||||
->where('filesize', '>', 0)
|
||||
->where('filesize', '<', $effectiveMinSize)
|
||||
->pluck('id')
|
||||
->all();
|
||||
$stats['minSize'] += $this->collectionCleanupService->deleteCollectionsAndDescendants(
|
||||
$ids,
|
||||
'Min-size cleanup',
|
||||
$this->echoCLI
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->settings->maxSizeToFormRelease > 0) {
|
||||
$stats['maxSize'] += Collection::query()
|
||||
->where('filecheck', CollectionFileCheckStatus::Sized->value)
|
||||
->where('filesize', '>', $this->settings->maxSizeToFormRelease)
|
||||
->delete();
|
||||
}
|
||||
if ($this->settings->maxSizeToFormRelease > 0) {
|
||||
$ids = Collection::query()
|
||||
->where('filecheck', CollectionFileCheckStatus::Sized->value)
|
||||
->where('filesize', '>', $this->settings->maxSizeToFormRelease)
|
||||
->pluck('id')
|
||||
->all();
|
||||
$stats['maxSize'] += $this->collectionCleanupService->deleteCollectionsAndDescendants(
|
||||
$ids,
|
||||
'Max-size cleanup',
|
||||
$this->echoCLI
|
||||
);
|
||||
}
|
||||
|
||||
$effectiveMinFiles = max($groupMinFiles, $this->settings->minFilesToFormRelease);
|
||||
if ($effectiveMinFiles > 0) {
|
||||
$stats['minFiles'] += Collection::query()
|
||||
->where('filecheck', CollectionFileCheckStatus::Sized->value)
|
||||
->where('filesize', '>', 0)
|
||||
->where('totalfiles', '<', $effectiveMinFiles)
|
||||
->delete();
|
||||
}
|
||||
}, 10);
|
||||
$effectiveMinFiles = max($groupMinFiles, $this->settings->minFilesToFormRelease);
|
||||
if ($effectiveMinFiles > 0) {
|
||||
$ids = Collection::query()
|
||||
->where('filecheck', CollectionFileCheckStatus::Sized->value)
|
||||
->where('filesize', '>', 0)
|
||||
->where('totalfiles', '<', $effectiveMinFiles)
|
||||
->pluck('id')
|
||||
->all();
|
||||
$stats['minFiles'] += $this->collectionCleanupService->deleteCollectionsAndDescendants(
|
||||
$ids,
|
||||
'Min-files cleanup',
|
||||
$this->echoCLI
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->outputCollectionDeleteStats($stats, $startTime);
|
||||
@@ -993,20 +1010,24 @@ final class ReleaseProcessingService
|
||||
|
||||
do {
|
||||
try {
|
||||
$groupCondition = $groupID !== 0 ? 'AND groups_id = ? ' : '';
|
||||
$batchLimit = self::BATCH_SIZE;
|
||||
$query = DB::table('collections')
|
||||
->where('added', '<', $cutoff)
|
||||
->orderBy('id')
|
||||
->limit(self::BATCH_SIZE);
|
||||
if ($groupID !== 0) {
|
||||
$query->where('groups_id', '=', $groupID);
|
||||
}
|
||||
|
||||
$sql = <<<SQL
|
||||
DELETE FROM collections WHERE id IN (
|
||||
SELECT id FROM (
|
||||
SELECT id FROM collections WHERE added < ? {$groupCondition}
|
||||
ORDER BY id LIMIT {$batchLimit}
|
||||
) AS x
|
||||
)
|
||||
SQL;
|
||||
$ids = $query->pluck('id')->all();
|
||||
if ($ids === []) {
|
||||
break;
|
||||
}
|
||||
|
||||
$params = $groupID !== 0 ? [$cutoff, $groupID] : [$cutoff];
|
||||
$affected = DB::affectingStatement($sql, $params);
|
||||
$affected = $this->collectionCleanupService->deleteCollectionsAndDescendants(
|
||||
$ids,
|
||||
'Stuck collections cleanup',
|
||||
$this->echoCLI
|
||||
);
|
||||
break;
|
||||
} catch (Throwable $e) {
|
||||
$attempt++;
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (DB::getDriverName() === 'sqlite') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! Schema::hasTable('collections') || ! Schema::hasTable('binaries') || ! Schema::hasTable('parts')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove orphans so FK creation cannot fail on existing bad rows.
|
||||
DB::statement(
|
||||
'DELETE p FROM parts p LEFT JOIN binaries b ON b.id = p.binaries_id WHERE b.id IS NULL'
|
||||
);
|
||||
DB::statement(
|
||||
'DELETE b FROM binaries b LEFT JOIN collections c ON c.id = b.collections_id WHERE c.id IS NULL'
|
||||
);
|
||||
|
||||
$this->dropForeignKeyIfExists('parts', 'FK_binaries');
|
||||
$this->dropForeignKeyIfExists('binaries', 'FK_Collections');
|
||||
|
||||
DB::statement(
|
||||
'ALTER TABLE `binaries`
|
||||
ADD CONSTRAINT `FK_Collections`
|
||||
FOREIGN KEY (`collections_id`)
|
||||
REFERENCES `collections` (`id`)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE'
|
||||
);
|
||||
|
||||
DB::statement(
|
||||
'ALTER TABLE `parts`
|
||||
ADD CONSTRAINT `FK_binaries`
|
||||
FOREIGN KEY (`binaries_id`)
|
||||
REFERENCES `binaries` (`id`)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE'
|
||||
);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (DB::getDriverName() === 'sqlite') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! Schema::hasTable('binaries') || ! Schema::hasTable('parts')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->dropForeignKeyIfExists('parts', 'FK_binaries');
|
||||
$this->dropForeignKeyIfExists('binaries', 'FK_Collections');
|
||||
}
|
||||
|
||||
private function dropForeignKeyIfExists(string $table, string $constraintName): void
|
||||
{
|
||||
$exists = DB::select(
|
||||
'SELECT CONSTRAINT_NAME
|
||||
FROM information_schema.TABLE_CONSTRAINTS
|
||||
WHERE CONSTRAINT_TYPE = "FOREIGN KEY"
|
||||
AND TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = ?
|
||||
AND CONSTRAINT_NAME = ?',
|
||||
[$table, $constraintName]
|
||||
);
|
||||
|
||||
if ($exists !== []) {
|
||||
DB::statement("ALTER TABLE `{$table}` DROP FOREIGN KEY `{$constraintName}`");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,283 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Enums\CollectionFileCheckStatus;
|
||||
use App\Models\Release;
|
||||
use App\Services\CollectionCleanupService;
|
||||
use App\Services\Nzb\NzbService;
|
||||
use App\Services\ReleaseCleaningService;
|
||||
use App\Services\ReleaseCreationService;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CbpCleanupServiceTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config(['database.default' => 'sqlite', 'database.connections.sqlite.database' => ':memory:']);
|
||||
DB::purge();
|
||||
DB::reconnect();
|
||||
DB::connection()->getPdo()->sqliteCreateFunction('UNIX_TIMESTAMP', static fn (?string $value): int => strtotime((string) $value));
|
||||
|
||||
$this->createTables();
|
||||
$this->seedSettings();
|
||||
}
|
||||
|
||||
public function test_retention_cleanup_deletes_parts_binaries_and_collections_without_fk_cascades(): void
|
||||
{
|
||||
DB::table('collections')->insert([
|
||||
'id' => 100,
|
||||
'subject' => 'Retention.Release',
|
||||
'fromname' => 'poster@example.com',
|
||||
'date' => now()->subHours(10)->format('Y-m-d H:i:s'),
|
||||
'dateadded' => now()->subHours(10)->format('Y-m-d H:i:s'),
|
||||
'added' => now()->subHours(10)->format('Y-m-d H:i:s'),
|
||||
'xref' => 'alt.test:123',
|
||||
'groups_id' => 1,
|
||||
'totalfiles' => 1,
|
||||
'filesize' => 500,
|
||||
'filecheck' => CollectionFileCheckStatus::Sized->value,
|
||||
'collectionhash' => 'retention-hash',
|
||||
'collection_regexes_id' => 0,
|
||||
'releases_id' => null,
|
||||
'noise' => '',
|
||||
]);
|
||||
DB::table('binaries')->insert([
|
||||
'id' => 1000,
|
||||
'name' => 'Retention.Release.par2',
|
||||
'collections_id' => 100,
|
||||
'totalparts' => 1,
|
||||
]);
|
||||
DB::table('parts')->insert([
|
||||
'binaries_id' => 1000,
|
||||
'number' => 1,
|
||||
'messageid' => '<retention-1@example.com>',
|
||||
'partnumber' => 1,
|
||||
'size' => 10,
|
||||
]);
|
||||
|
||||
app(CollectionCleanupService::class)->deleteFinishedAndOrphans(false);
|
||||
|
||||
$this->assertSame(0, DB::table('parts')->count());
|
||||
$this->assertSame(0, DB::table('binaries')->count());
|
||||
$this->assertSame(0, DB::table('collections')->count());
|
||||
}
|
||||
|
||||
public function test_nzb_creation_cleans_up_collection_binary_and_parts_explicitly(): void
|
||||
{
|
||||
DB::table('releases')->insert([
|
||||
'id' => 1,
|
||||
'name' => 'Nzb.Release',
|
||||
'searchname' => 'Nzb.Release',
|
||||
'totalpart' => 1,
|
||||
'groups_id' => 1,
|
||||
'adddate' => now()->format('Y-m-d H:i:s'),
|
||||
'guid' => str_repeat('a', 36),
|
||||
'leftguid' => 'a',
|
||||
'postdate' => now()->format('Y-m-d H:i:s'),
|
||||
'fromname' => 'poster@example.com',
|
||||
'size' => 500,
|
||||
'passwordstatus' => 0,
|
||||
'haspreview' => -1,
|
||||
'categories_id' => 1,
|
||||
'nfostatus' => -1,
|
||||
'nzbstatus' => NzbService::NZB_NONE,
|
||||
'isrenamed' => 1,
|
||||
'iscategorized' => 1,
|
||||
'predb_id' => 0,
|
||||
'source' => null,
|
||||
'nzb_guid' => null,
|
||||
]);
|
||||
|
||||
DB::table('collections')->insert([
|
||||
'id' => 200,
|
||||
'subject' => 'Nzb.Release',
|
||||
'fromname' => 'poster@example.com',
|
||||
'date' => now()->subHours(1)->format('Y-m-d H:i:s'),
|
||||
'dateadded' => now()->subHours(1)->format('Y-m-d H:i:s'),
|
||||
'added' => now()->subHours(1)->format('Y-m-d H:i:s'),
|
||||
'xref' => 'alt.test:200',
|
||||
'groups_id' => 1,
|
||||
'totalfiles' => 1,
|
||||
'filesize' => 500,
|
||||
'filecheck' => CollectionFileCheckStatus::Inserted->value,
|
||||
'collectionhash' => 'nzb-hash',
|
||||
'collection_regexes_id' => 0,
|
||||
'releases_id' => 1,
|
||||
'noise' => '',
|
||||
]);
|
||||
DB::table('binaries')->insert([
|
||||
'id' => 2000,
|
||||
'name' => 'Nzb.Release yEnc',
|
||||
'collections_id' => 200,
|
||||
'totalparts' => 1,
|
||||
]);
|
||||
DB::table('parts')->insert([
|
||||
'binaries_id' => 2000,
|
||||
'number' => 1,
|
||||
'messageid' => '<nzb-1@example.com>',
|
||||
'partnumber' => 1,
|
||||
'size' => 10,
|
||||
]);
|
||||
|
||||
$release = Release::query()->findOrFail(1);
|
||||
$release->setRelation('category', (object) ['title' => 'Misc', 'parent' => (object) ['title' => 'Other']]);
|
||||
|
||||
$written = app(NzbService::class)->writeNzbForReleaseId($release);
|
||||
|
||||
$this->assertTrue($written);
|
||||
$this->assertSame(0, DB::table('parts')->count());
|
||||
$this->assertSame(0, DB::table('binaries')->count());
|
||||
$this->assertSame(0, DB::table('collections')->count());
|
||||
$this->assertSame(1, (int) DB::table('releases')->where('id', 1)->value('nzbstatus'));
|
||||
}
|
||||
|
||||
public function test_duplicate_release_path_cleans_up_collection_binary_and_parts(): void
|
||||
{
|
||||
DB::table('releases')->insert([
|
||||
'id' => 2,
|
||||
'name' => 'Duplicate.Release',
|
||||
'searchname' => 'Duplicate.Release',
|
||||
'totalpart' => 1,
|
||||
'groups_id' => 1,
|
||||
'adddate' => now()->format('Y-m-d H:i:s'),
|
||||
'guid' => str_repeat('b', 36),
|
||||
'leftguid' => 'b',
|
||||
'postdate' => now()->format('Y-m-d H:i:s'),
|
||||
'fromname' => 'poster@example.com',
|
||||
'size' => 1000,
|
||||
'passwordstatus' => 0,
|
||||
'haspreview' => -1,
|
||||
'categories_id' => 1,
|
||||
'nfostatus' => -1,
|
||||
'nzbstatus' => NzbService::NZB_NONE,
|
||||
'isrenamed' => 1,
|
||||
'iscategorized' => 1,
|
||||
'predb_id' => 0,
|
||||
'source' => null,
|
||||
'nzb_guid' => null,
|
||||
]);
|
||||
|
||||
DB::table('collections')->insert([
|
||||
'id' => 300,
|
||||
'subject' => 'Duplicate.Release',
|
||||
'fromname' => 'poster@example.com',
|
||||
'date' => now()->subHours(1)->format('Y-m-d H:i:s'),
|
||||
'dateadded' => now()->subHours(1)->format('Y-m-d H:i:s'),
|
||||
'added' => now()->subHours(1)->format('Y-m-d H:i:s'),
|
||||
'xref' => 'alt.test:300',
|
||||
'groups_id' => 1,
|
||||
'totalfiles' => 1,
|
||||
'filesize' => 1000,
|
||||
'filecheck' => CollectionFileCheckStatus::Sized->value,
|
||||
'collectionhash' => 'duplicate-hash',
|
||||
'collection_regexes_id' => 0,
|
||||
'releases_id' => null,
|
||||
'noise' => '',
|
||||
]);
|
||||
DB::table('binaries')->insert([
|
||||
'id' => 3000,
|
||||
'name' => 'Duplicate.Release yEnc',
|
||||
'collections_id' => 300,
|
||||
'totalparts' => 1,
|
||||
]);
|
||||
DB::table('parts')->insert([
|
||||
'binaries_id' => 3000,
|
||||
'number' => 1,
|
||||
'messageid' => '<duplicate-1@example.com>',
|
||||
'partnumber' => 1,
|
||||
'size' => 10,
|
||||
]);
|
||||
|
||||
$service = new ReleaseCreationService(
|
||||
app(ReleaseCleaningService::class),
|
||||
app(CollectionCleanupService::class)
|
||||
);
|
||||
$result = $service->createReleases(null, 10, false);
|
||||
|
||||
$this->assertSame(['added' => 0, 'dupes' => 1], $result);
|
||||
$this->assertSame(0, DB::table('parts')->count());
|
||||
$this->assertSame(0, DB::table('binaries')->count());
|
||||
$this->assertSame(0, DB::table('collections')->count());
|
||||
}
|
||||
|
||||
private function seedSettings(): void
|
||||
{
|
||||
$settings = [
|
||||
'partretentionhours' => '1',
|
||||
'nzbsplitlevel' => '1',
|
||||
'check_passworded_rars' => '0',
|
||||
'categorizeforeign' => '1',
|
||||
'catwebdl' => '1',
|
||||
];
|
||||
|
||||
foreach ($settings as $name => $value) {
|
||||
DB::table('settings')->insert(['name' => $name, 'value' => $value]);
|
||||
}
|
||||
}
|
||||
|
||||
private function createTables(): void
|
||||
{
|
||||
DB::statement('CREATE TABLE settings (name VARCHAR(255) PRIMARY KEY, value TEXT)');
|
||||
DB::statement('CREATE TABLE usenet_groups (id INTEGER PRIMARY KEY, name VARCHAR(255))');
|
||||
DB::statement('CREATE TABLE categories (id INTEGER PRIMARY KEY, title VARCHAR(255), parent_categories_id INTEGER NULL)');
|
||||
DB::statement('CREATE TABLE releases (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name VARCHAR(255),
|
||||
searchname VARCHAR(255),
|
||||
totalpart INTEGER,
|
||||
groups_id INTEGER,
|
||||
adddate DATETIME NULL,
|
||||
guid VARCHAR(64),
|
||||
leftguid VARCHAR(1),
|
||||
postdate DATETIME NULL,
|
||||
fromname VARCHAR(255),
|
||||
size INTEGER,
|
||||
passwordstatus INTEGER,
|
||||
haspreview INTEGER,
|
||||
categories_id INTEGER,
|
||||
nfostatus INTEGER,
|
||||
nzbstatus INTEGER,
|
||||
isrenamed INTEGER,
|
||||
iscategorized INTEGER,
|
||||
predb_id INTEGER,
|
||||
source VARCHAR(255) NULL,
|
||||
nzb_guid BLOB NULL
|
||||
)');
|
||||
DB::statement('CREATE TABLE collections (
|
||||
id INTEGER PRIMARY KEY,
|
||||
subject VARCHAR(255),
|
||||
fromname VARCHAR(255),
|
||||
date DATETIME NULL,
|
||||
dateadded DATETIME NULL,
|
||||
added DATETIME NULL,
|
||||
xref TEXT,
|
||||
groups_id INTEGER,
|
||||
totalfiles INTEGER,
|
||||
filesize INTEGER,
|
||||
filecheck INTEGER,
|
||||
collectionhash VARCHAR(255),
|
||||
collection_regexes_id INTEGER,
|
||||
releases_id INTEGER NULL,
|
||||
noise VARCHAR(64)
|
||||
)');
|
||||
DB::statement('CREATE TABLE binaries (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name VARCHAR(255),
|
||||
collections_id INTEGER,
|
||||
totalparts INTEGER
|
||||
)');
|
||||
DB::statement('CREATE TABLE parts (
|
||||
binaries_id INTEGER,
|
||||
number INTEGER,
|
||||
messageid VARCHAR(255),
|
||||
partnumber INTEGER,
|
||||
size INTEGER
|
||||
)');
|
||||
DB::table('usenet_groups')->insert(['id' => 1, 'name' => 'alt.test']);
|
||||
DB::table('categories')->insert(['id' => 1, 'title' => 'Misc', 'parent_categories_id' => null]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user