1010
1111use App \Actions \Shop \PurchasableService ;
1212use App \Constants \PhotoAlbum as PA ;
13- use App \Enum \SizeVariantType ;
14- use App \Enum \StorageDiskType ;
13+ use App \DTO \Delete \PhotosToBeDeletedDTO ;
1514use App \Events \PhotoDeleted ;
1615use App \Exceptions \Internal \LycheeLogicException ;
17- use App \Exceptions \Internal \QueryBuilderException ;
1816use App \Exceptions \ModelDBException ;
19- use App \Jobs \FileDeleterJob ;
20- use App \Models \SizeVariant ;
21- use Illuminate \Database \Query \JoinClause ;
22- use Illuminate \Support \Collection ;
2317use Illuminate \Support \Facades \DB ;
2418
2519/**
@@ -53,66 +47,6 @@ public function __construct(
5347 $ this ->purchasable_service = resolve (PurchasableService::class);
5448 }
5549
56- /**
57- * Delete the designated photos.
58- * There is no check for album dependencies. This has already be done.
59- * This is a force delete.
60- *
61- * @param array $photo_ids Photos to deleted
62- *
63- * @return array
64- */
65- public function forceDelete (array $ photo_ids ): array
66- {
67- // We do not check the purchasable service as they have already been deleted by the caller.
68- if (count ($ photo_ids ) === 0 ) {
69- return [];
70- }
71-
72- // Reset headers and covers pointing to deleted photos
73- DB ::table ('albums ' )->whereIn ('header_id ' , $ photo_ids )->update (['header_id ' => null ]);
74- DB ::table ('albums ' )->whereIn ('cover_id ' , $ photo_ids )->update (['cover_id ' => null ]);
75-
76- // Maybe consider doing multiple queries for the different storage types.
77- $ exclude_size_variants_ids = DB ::table ('order_items ' )->select (['size_variant_id ' ])->pluck ('size_variant_id ' )->all ();
78-
79- // Collect size variants to be deleted
80- // ! Risk of memory exhaustion if too many photos are deleted at once !
81- $ size_variants_local = $ this ->collectSizeVariantPathsByPhotoID ($ photo_ids , StorageDiskType::LOCAL , $ exclude_size_variants_ids );
82- $ short_paths_local = $ size_variants_local ->pluck ('short_path ' )->all ();
83- $ short_path_watermarked_local = $ size_variants_local ->pluck ('short_path_watermarked ' )->filter ()->all ();
84-
85- $ size_variants_s3 = $ this ->collectSizeVariantPathsByPhotoID ($ photo_ids , StorageDiskType::S3 , $ exclude_size_variants_ids );
86- $ short_paths_s3 = $ size_variants_s3 ->pluck ('short_path ' )->all ();
87- $ short_path_watermarked_s3 = $ size_variants_s3 ->pluck ('short_path_watermarked ' )->filter ()->all ();
88-
89- $ live_photo_short_paths_local = $ this ->collectLivePhotoPathsByPhotoID ($ photo_ids , StorageDiskType::LOCAL )->pluck ('live_photo_short_path ' )->all ();
90- $ live_photo_short_paths_s3 = $ this ->collectLivePhotoPathsByPhotoID ($ photo_ids , StorageDiskType::S3 )->pluck ('live_photo_short_path ' )->all ();
91-
92- $ delete_jobs = [];
93- $ delete_jobs [] = new FileDeleterJob (StorageDiskType::LOCAL , $ short_paths_local );
94- $ delete_jobs [] = new FileDeleterJob (StorageDiskType::LOCAL , $ short_path_watermarked_local );
95- $ delete_jobs [] = new FileDeleterJob (StorageDiskType::LOCAL , $ live_photo_short_paths_local );
96- $ delete_jobs [] = new FileDeleterJob (StorageDiskType::S3 , $ short_paths_s3 );
97- $ delete_jobs [] = new FileDeleterJob (StorageDiskType::S3 , $ short_path_watermarked_s3 );
98- $ delete_jobs [] = new FileDeleterJob (StorageDiskType::S3 , $ live_photo_short_paths_s3 );
99-
100- // Now delete DB records
101- // ! If we are deleting more than a few 1000 photos at once, we may run into
102- // ! SQL query size limits. In that case, we need to chunk the deletion.
103-
104- // Those we are keeping.
105- DB ::table ('size_variants ' )->whereIn ('id ' , $ exclude_size_variants_ids )->update (['photo_id ' => null ]);
106- // Those we delete
107- DB ::table ('size_variants ' )->whereIn ('photo_id ' , $ photo_ids )->delete ();
108- DB ::table ('statistics ' )->whereIn ('photo_id ' , $ photo_ids )->delete ();
109- DB ::table ('palettes ' )->whereIn ('photo_id ' , $ photo_ids )->delete ();
110- DB ::table ('photo_album ' )->whereIn ('photo_id ' , $ photo_ids )->delete (); // Just to be sure.
111- DB ::table ('photos ' )->whereIn ('id ' , $ photo_ids )->delete ();
112-
113- return $ delete_jobs ;
114- }
115-
11650 /**
11751 * Deletes the designated photos from the DB.
11852 *
@@ -172,11 +106,14 @@ public function do(array $photo_ids, string|null $from_id): void
172106 // Finally, add the unsorted photos to be deleted.
173107 $ delete_photo_ids = array_merge ($ delete_photo_ids , $ unsorted_photo_ids );
174108
175- // We delete the photos which are in the from and listed in albums.
176- DB ::table (PA ::PHOTO_ALBUM )->whereIn (PA ::PHOTO_ID , $ photo_ids )->where (PA ::ALBUM_ID , '= ' , $ from_id )->delete ();
177-
178109 $ this ->purchasable_service ->deleteMulitplePhotoPurchasables ($ photo_ids , [$ from_id ]);
179- $ jobs = $ this ->forceDelete ($ delete_photo_ids );
110+
111+ $ photos_to_be_deleted = new PhotosToBeDeletedDTO (
112+ force_delete_photo_ids: $ delete_photo_ids ,
113+ soft_delete_photo_ids: $ photo_ids ,
114+ album_ids: [$ from_id ],
115+ );
116+ $ jobs = $ photos_to_be_deleted ->executeDelete ();
180117
181118 // Dispatch events for affected albums to trigger recomputation
182119 PhotoDeleted::dispatch ($ from_id );
@@ -185,69 +122,4 @@ public function do(array $photo_ids, string|null $from_id): void
185122 dispatch ($ job );
186123 }
187124 }
188-
189- /**
190- * Collects all short paths of size variants which shall be deleted from
191- * disk.
192- *
193- * Size variants which belong to a photo which has a duplicate that is
194- * not going to be deleted are skipped.
195- *
196- * @param array<int,string> $photo_ids the photo IDs
197- * @param StorageDiskType $storage_disk the storage disk to filter for (null = all)
198- * @param array<int,string> $exclude_size_variants_ids size variant IDs to be excluded
199- *
200- * @return Collection<int,SizeVariant> the size variants to be deleted
201- *
202- * @throws QueryBuilderException
203- */
204- private function collectSizeVariantPathsByPhotoID (array $ photo_ids , StorageDiskType $ storage_disk , array $ exclude_size_variants_ids ): Collection
205- {
206- if (count ($ photo_ids ) === 0 ) {
207- return collect ([]);
208- }
209-
210- return SizeVariant::query ()
211- ->from ('size_variants as sv ' )
212- ->select (['sv.short_path ' , 'sv.short_path_watermarked ' ])
213- ->join ('photos as p ' , 'p.id ' , '= ' , 'sv.photo_id ' )
214- ->whereIn ('p.id ' , $ photo_ids )
215- ->where ('sv.storage_disk ' , '= ' , $ storage_disk ->value )
216- ->whereNotIn ('sv.id ' , $ exclude_size_variants_ids )
217- ->toBase ()
218- ->get ();
219- }
220-
221- /**
222- * Collects all short paths of live photos which shall be deleted from
223- * disk.
224- *
225- * Live photos which have a duplicate that is not going to be deleted are
226- * skipped.
227- *
228- * @param array<int,string> $photo_ids the photo IDs
229- * @param StorageDiskType $storage_disk the storage disk to filter for (null = all)
230- *
231- * @return Collection<int,object{live_photo_short_path:string}> the live photo short paths to be deleted
232- *
233- * @throws QueryBuilderException
234- */
235- private function collectLivePhotoPathsByPhotoID (array $ photo_ids , StorageDiskType $ storage_disk ): Collection
236- {
237- if (count ($ photo_ids ) === 0 ) {
238- return collect ([]);
239- }
240-
241- return DB ::table ('photos ' , 'p ' )
242- ->select (['p.live_photo_short_path ' ])
243- ->join ('size_variants as sv ' , function (JoinClause $ join ): void {
244- $ join
245- ->on ('sv.photo_id ' , '= ' , 'p.id ' )
246- ->where ('sv.type ' , '= ' , SizeVariantType::ORIGINAL );
247- })
248- ->whereIn ('p.id ' , $ photo_ids )
249- ->whereNotNull ('p.live_photo_short_path ' )
250- ->where ('sv.storage_disk ' , '= ' , $ storage_disk ->value )
251- ->get ();
252- }
253125}
0 commit comments