Skip to content

Commit

Permalink
Merge pull request #154 from wri/feat/TM-804-bulk-delete
Browse files Browse the repository at this point in the history
[TM-804] Media bulk deletion
  • Loading branch information
roguenet authored Apr 22, 2024
2 parents ff65dc7 + 8d8873f commit 75d29aa
Show file tree
Hide file tree
Showing 8 changed files with 157 additions and 5 deletions.
9 changes: 8 additions & 1 deletion app/Http/Controllers/V2/Files/UploadController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use App\Models\V2\MediaModel;
use Exception;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

Expand Down Expand Up @@ -47,7 +48,12 @@ public function bulkUrlUpload(BulkUploadRequest $request, string $collection, Me
// The downloadable file gets shuttled through the internals of Spatie without a chance for us to run
// our own validations on them. png/jpg are the only mimes allowed for the photos collection according
// to config/file-handling.php, and we disallow other collections than 'photos' above.
$handler = $mediaModel->addMediaFromUrl($data['download_url'], 'image/png', 'image/jpg');
$handler = $mediaModel->addMediaFromUrl(
$data['download_url'],
'image/png',
'image/jpg',
'image/jpeg'
);

$this->prepHandler($handler, $data, $mediaModel, $config, $collection);
$details = $this->executeHandler($handler, $collection);
Expand Down Expand Up @@ -127,6 +133,7 @@ private function saveAdditionalFileProperties($media, $data, $config)
{
$media->file_type = $this->getType($media, $config);
$media->is_public = $data['is_public'] ?? true;
$media->created_by = Auth::user()->id;
$media->save();
}

Expand Down
26 changes: 26 additions & 0 deletions app/Http/Controllers/V2/MediaController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
namespace App\Http\Controllers\V2;

use App\Http\Controllers\Controller;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class MediaController extends Controller
{
Expand All @@ -32,4 +35,27 @@ public function delete(Request $request, string $uuid, string $collection = ''):

return response()->json(['success' => 'media has been deleted'], 202);
}

public function bulkDelete(Request $request): JsonResponse
{
if (! Auth::user()->can('media-manage')) {
throw new AuthorizationException('No permission to bulk delete');
}

$uuids = $request->input('uuids');
if (empty($uuids)) {
throw new NotFoundHttpException();
}

$media = Media::whereIn('uuid', $uuids)->where('created_by', Auth::user()->id);
if ($media->count() != count($uuids)) {
// If the bulk delete endpoint is being called for some media that weren't created by this user,
// avoid deleting any of them.
throw new AuthorizationException('Some of the media you are trying to delete were not created by you.');
}

$media->delete();

return response()->json(['success' => 'media has been deleted'], 202);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

use App\Models\V2\User;
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('media', function (Blueprint $table) {
$table->foreignIdFor(User::class, 'created_by')->nullable();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('media', function (Blueprint $table) {
$table->dropColumn(['created_by']);
});
}
};
16 changes: 16 additions & 0 deletions openapi-src/V2/paths/_index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,22 @@
type: array
items:
$ref: '../definitions/_index.yml#/V2FileRead'
/v2/media:
delete:
summary: Bulk delete a set of media by UUID
operationId: v2-bulk-delete-media
tags:
- Media
parameters:
- type: array
name: uuids[]
in: query
required: true
items:
type: string
responses:
'200':
description: OK
/v2/files/{UUID}:
put:
summary: Update properties of a specific file
Expand Down
16 changes: 16 additions & 0 deletions resources/docs/swagger-v2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69769,6 +69769,22 @@ paths:
type: boolean
created_at:
type: string
/v2/media:
delete:
summary: Bulk delete a set of media by UUID
operationId: v2-bulk-delete-media
tags:
- Media
parameters:
- type: array
name: 'uuids[]'
in: query
required: true
items:
type: string
responses:
'200':
description: OK
'/v2/files/{UUID}':
put:
summary: Update properties of a specific file
Expand Down
1 change: 1 addition & 0 deletions routes/api_v2.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@
});

Route::prefix('media')->group(function () {
Route::delete('', [MediaController::class, 'bulkDelete']);
Route::delete('/{uuid}', [MediaController::class, 'delete']);
Route::delete('/{uuid}/{collection}', [MediaController::class, 'delete']);
});
Expand Down
6 changes: 2 additions & 4 deletions tests/V2/Files/UploadControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -546,10 +546,8 @@ public function test_bulk_upload_functionality()
$service = User::factory()->serviceAccount()->create();
Artisan::call('v2migration:roles');
$site = Site::factory()->create();
// It's not ideal for the testing suite to use a real hosted image, but I haven't found a way to fake a
// http download URL in phpunit/spatie.
$url = 'https://new-wri-prod.wri-restoration-marketplace-api.com/images/V2/land-tenures/national-protected-area.png';
$badMimeUrl = 'https://www.terramatch.org/images/landing-page-hero-banner.webp';
$url = 'http://localhost/images/V2/land-tenures/national-protected-area.png';
$badMimeUrl = 'http://localhost/images/email_logo.gif';

// Check a valid upload
$this->actingAs($service)
Expand Down
60 changes: 60 additions & 0 deletions tests/V2/MediaControllerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace Tests\V2;

use App\Models\User;
use App\Models\V2\Sites\Site;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Artisan;
use Tests\TestCase;

final class MediaControllerTest extends TestCase
{
use RefreshDatabase;
use WithFaker;

public function test_bulk_delete(): void
{
$service = User::factory()->serviceAccount()->create();
$admin = User::factory()->admin()->create();
Artisan::call('v2migration:roles');

$site = Site::factory()->ppc()->create();
$photo1 = $site->addMedia(UploadedFile::fake()->image('photo1'))->toMediaCollection('photos');
$photo1->update(['created_by' => $service->id]);
$photo2 = $site->addMedia(UploadedFile::fake()->image('photo2'))->toMediaCollection('photos');
$photo2->update(['created_by' => $admin->id]);
$photo3 = $site->addMedia(UploadedFile::fake()->image('photo3'))->toMediaCollection('photos');
$photo3->update(['created_by' => $service->id]);

// No UUIDS is a 404
$this->actingAs($service)
->delete('/api/v2/media')
->assertNotFound();

// Can't delete photo created by admin
$this->actingAs($service)
->delete($this->buildBulkDeleteUrl([$photo1->uuid, $photo2->uuid]))
->assertForbidden();
$this->assertEquals(3, $site->refresh()->getMedia('photos')->count());

// Only service accounts can use bulk delete
$this->actingAs($admin)
->delete($this->buildBulkDeleteUrl([$photo2->uuid]))
->assertForbidden();
$this->assertEquals(3, $site->refresh()->getMedia('photos')->count());

// Success case
$this->actingAs($service)
->delete($this->buildBulkDeleteUrl([$photo1->uuid, $photo3->uuid]))
->assertSuccessful();
$this->assertEquals(1, $site->refresh()->getMedia('photos')->count());
}

private function buildBulkDeleteUrl($uuids): string
{
return '/api/v2/media?uuids[]=' . implode('&uuids[]=', $uuids);
}
}

0 comments on commit 75d29aa

Please sign in to comment.