Page MenuHomePhorge

D5775.1775244544.diff
No OneTemporary

Authored By
Unknown
Size
7 KB
Referenced Files
None
Subscribers
None

D5775.1775244544.diff

diff --git a/src/app/Console/Commands/Fs/ExpungeCommand.php b/src/app/Console/Commands/Fs/ExpungeCommand.php
--- a/src/app/Console/Commands/Fs/ExpungeCommand.php
+++ b/src/app/Console/Commands/Fs/ExpungeCommand.php
@@ -38,12 +38,24 @@
->orderBy('deleted_at')
->cursor();
- // FIXME: Should we use an async job for each file?
+ foreach ($files as $file) {
+ Storage::fileDelete($file);
+ }
+
+ // Remove orphaned files
+ $files = Item::withTrashed()
+ ->where('type', '&', Item::TYPE_FILE)
+ ->whereNull('user_id')
+ ->orderBy('deleted_at')
+ ->cursor();
foreach ($files as $file) {
Storage::fileDelete($file);
}
+ // Remove orphaned items (of any type)
+ Item::withTrashed()->whereNull('user_id')->forceDelete();
+
// We remove orphaned chunks after upload ttl (while marked as deleted some chunks may still be
// in the process of uploading a file).
$chunks = Chunk::withTrashed()
@@ -51,10 +63,10 @@
->orderBy('deleted_at')
->cursor();
- // FIXME: Should we use an async job for each chunk?
-
foreach ($chunks as $chunk) {
Storage::fileChunkDelete($chunk);
}
+
+ // FIXME: What about soft-deleted collection items, should we remove them (after some time)?
}
}
diff --git a/src/app/Console/Commands/User/DelegateCommand.php b/src/app/Console/Commands/User/DelegateCommand.php
--- a/src/app/Console/Commands/User/DelegateCommand.php
+++ b/src/app/Console/Commands/User/DelegateCommand.php
@@ -4,7 +4,6 @@
use App\Console\Command;
use App\Delegation;
-use Carbon\Carbon;
class DelegateCommand extends Command
{
diff --git a/src/database/migrations/2025_12_30_100000_update_fs_items_table.php b/src/database/migrations/2025_12_30_100000_update_fs_items_table.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2025_12_30_100000_update_fs_items_table.php
@@ -0,0 +1,37 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration {
+ /**
+ * Run the migrations.
+ */
+ public function up(): void
+ {
+ Schema::table(
+ 'fs_items',
+ static function (Blueprint $table) {
+ $table->dropForeign(['user_id']);
+ $table->bigInteger('user_id')->nullable()->change();
+ $table->foreign('user_id')->references('id')->on('users')->cascadeOnUpdate()->nullOnDelete();
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table(
+ 'fs_items',
+ static function (Blueprint $table) {
+ $table->dropForeign(['user_id']);
+ $table->bigInteger('user_id')->change();
+ $table->foreign('user_id')->references('id')->on('users')->cascadeOnUpdate()->cascadeOnDelete();
+ }
+ );
+ }
+};
diff --git a/src/tests/Feature/Backends/StorageTest.php b/src/tests/Feature/Backends/StorageTest.php
--- a/src/tests/Feature/Backends/StorageTest.php
+++ b/src/tests/Feature/Backends/StorageTest.php
@@ -3,12 +3,51 @@
namespace Tests\Feature\Backends;
use App\Backends\Storage;
+use App\Fs\Chunk;
use App\Fs\Item;
use Illuminate\Support\Facades\Storage as LaravelStorage;
use Tests\TestCaseFs;
class StorageTest extends TestCaseFs
{
+ /**
+ * Test Storage::fileChunkDelete()
+ */
+ public function testFileChunkDelete(): void
+ {
+ $disk = LaravelStorage::disk(\config('filesystems.default'));
+ $user = $this->getTestUser('john@kolab.org');
+ $file = $this->getTestFile($user, 'test.txt', 'Test content1', ['mimetype' => 'text/plain']);
+
+ $chunk = $file->chunks()->first();
+ $path = Storage::chunkLocation($chunk->chunk_id, $file);
+
+ $this->assertTrue(LaravelStorage::fileExists($path));
+
+ Storage::fileChunkDelete($chunk);
+
+ $this->assertFalse(LaravelStorage::fileExists($path));
+ $this->assertNull(Chunk::find($chunk->id));
+ }
+
+ /**
+ * Test Storage::fileDelete()
+ */
+ public function testFileDelete(): void
+ {
+ $disk = LaravelStorage::disk(\config('filesystems.default'));
+ $user = $this->getTestUser('john@kolab.org');
+ $file = $this->getTestFile($user, 'test.txt', 'Test content1', ['mimetype' => 'text/plain']);
+
+ $path = $file->path . '/' . $file->id;
+ $this->assertTrue(LaravelStorage::directoryExists($path));
+
+ Storage::fileDelete($file);
+
+ $this->assertFalse(LaravelStorage::directoryExists($path));
+ $this->assertNull(Item::find($file->id));
+ }
+
/**
* Test Storage::fileInput() splitting the input into chunks
*/
diff --git a/src/tests/Feature/Console/Fs/ExpungeTest.php b/src/tests/Feature/Console/Fs/ExpungeTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/Fs/ExpungeTest.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Tests\Feature\Console\Fs;
+
+use App\Backends\Storage;
+use App\Fs\Chunk;
+use App\Fs\Item;
+use Tests\TestCaseFs;
+
+class ExpungeTest extends TestCaseFs
+{
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('expungetest@kolabnow.com');
+ }
+
+ protected function tearDown(): void
+ {
+ $this->deleteTestUser('expungetest@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test fs:expunge command
+ */
+ public function testHandle(): void
+ {
+ $user = $this->getTestUser('expungetest@kolabnow.com');
+ $folder1 = $this->getTestCollection($user, 'folder');
+ $file1 = $this->getTestFile($user, 'test1.txt', 'Test content1', ['mimetype' => 'text/plain']);
+ $file2 = $this->getTestFile($user, 'test2.txt', 'Test content2', ['mimetype' => 'text/plain']);
+
+ // Expect no changes
+ $code = \Artisan::call('fs:expunge');
+ $this->assertSame(0, $code);
+ $this->assertSame('', trim(\Artisan::output()));
+ $this->assertCount(3, $user->fsItems()->get());
+
+ // Test expunging of soft-deleted files
+ $file1->delete();
+
+ $code = \Artisan::call('fs:expunge');
+ $this->assertSame(0, $code);
+ $this->assertNull(Item::find($file1->id));
+ $this->assertCount(2, $user->fsItems()->get());
+
+ // TODO: Test expunging of orphaned chunks
+ $file2->chunks()->create([
+ 'chunk_id' => '123456789',
+ 'sequence' => 2,
+ 'size' => 2,
+ ]);
+ $chunk = Chunk::where('chunk_id', '123456789')->first();
+ $chunk->update(['deleted_at' => now()->subSeconds(Storage::UPLOAD_TTL + 5)]);
+
+ $code = \Artisan::call('fs:expunge');
+ $this->assertSame(0, $code);
+ $this->assertNull(Chunk::where('chunk_id', $chunk->chunk_id)->first());
+
+ // Delete user (this does not remove files)
+ $user->delete();
+ $this->assertCount(2, $user->fsItems()->get());
+
+ $code = \Artisan::call('fs:expunge');
+ $this->assertSame(0, $code);
+ $this->assertCount(2, $user->fsItems()->get());
+
+ // Force-delete user (should orphan files/collections and then expunge them)
+ $user->forceDelete();
+ $this->assertCount(2, Item::whereNull('user_id')->get());
+
+ $code = \Artisan::call('fs:expunge');
+ $this->assertSame(0, $code);
+ $this->assertCount(0, Item::all());
+ }
+}
diff --git a/src/tests/Feature/Console/User/DelegateTest.php b/src/tests/Feature/Console/User/DelegateTest.php
--- a/src/tests/Feature/Console/User/DelegateTest.php
+++ b/src/tests/Feature/Console/User/DelegateTest.php
@@ -4,7 +4,6 @@
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
-use App\Delegation;
class DelegateTest extends TestCase
{

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 3, 7:29 PM (17 h, 55 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18826002
Default Alt Text
D5775.1775244544.diff (7 KB)

Event Timeline