MediaS3GarbageCollector.php 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. <?php
  2. namespace App\Console\Commands;
  3. use Illuminate\Console\Command;
  4. use App\Media;
  5. use App\Status;
  6. use Illuminate\Support\Facades\Log;
  7. use Illuminate\Support\Facades\Storage;
  8. use App\Services\MediaService;
  9. use App\Services\StatusService;
  10. use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
  11. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  12. class MediaS3GarbageCollector extends Command
  13. {
  14. /**
  15. * The name and signature of the console command.
  16. *
  17. * @var string
  18. */
  19. protected $signature = 'media:s3gc {--limit=200} {--huge} {--log-errors}';
  20. /**
  21. * The console command description.
  22. *
  23. * @var string
  24. */
  25. protected $description = 'Delete (local) media uploads that exist on S3';
  26. /**
  27. * Create a new command instance.
  28. *
  29. * @return void
  30. */
  31. public function __construct()
  32. {
  33. parent::__construct();
  34. }
  35. /**
  36. * Execute the console command.
  37. *
  38. * @return int
  39. */
  40. public function handle()
  41. {
  42. $enabled = in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']);
  43. if(!$enabled) {
  44. $this->error('Cloud storage not enabled. Exiting...');
  45. return;
  46. }
  47. $deleteEnabled = config('media.delete_local_after_cloud');
  48. if(!$deleteEnabled) {
  49. $this->error('Delete local storage after cloud upload is not enabled');
  50. return;
  51. }
  52. $limit = $this->option('limit');
  53. $hugeMode = $this->option('huge');
  54. $log = $this->option('log-errors');
  55. if($limit > 2000 && !$hugeMode) {
  56. $this->error('Limit exceeded, please use a limit under 2000 or run again with the --huge flag');
  57. return;
  58. }
  59. $minId = Media::orderByDesc('id')->where('created_at', '<', now()->subHours(12))->first();
  60. if(!$minId) {
  61. return;
  62. } else {
  63. $minId = $minId->id;
  64. }
  65. return $hugeMode ?
  66. $this->hugeMode($minId, $limit, $log) :
  67. $this->regularMode($minId, $limit, $log);
  68. }
  69. protected function regularMode($minId, $limit, $log)
  70. {
  71. $gc = Media::whereRemoteMedia(false)
  72. ->whereNotNull(['status_id', 'cdn_url', 'replicated_at'])
  73. ->whereNot('version', '4')
  74. ->where('id', '<', $minId)
  75. ->inRandomOrder()
  76. ->take($limit)
  77. ->get();
  78. $totalSize = 0;
  79. $bar = $this->output->createProgressBar($gc->count());
  80. $bar->start();
  81. $cloudDisk = Storage::disk(config('filesystems.cloud'));
  82. $localDisk = Storage::disk('local');
  83. foreach($gc as $media) {
  84. try {
  85. if(
  86. $cloudDisk->exists($media->media_path)
  87. ) {
  88. if( $localDisk->exists($media->media_path)) {
  89. $localDisk->delete($media->media_path);
  90. $media->version = 4;
  91. $media->save();
  92. $totalSize = $totalSize + $media->size;
  93. MediaService::del($media->status_id);
  94. StatusService::del($media->status_id, false);
  95. if($localDisk->exists($media->thumbnail_path)) {
  96. $localDisk->delete($media->thumbnail_path);
  97. }
  98. } else {
  99. $media->version = 4;
  100. $media->save();
  101. }
  102. } else {
  103. if($log) {
  104. Log::channel('media')->info('[GC] Local media not properly persisted to cloud storage', ['media_id' => $media->id]);
  105. }
  106. }
  107. $bar->advance();
  108. } catch (FileNotFoundException $e) {
  109. $bar->advance();
  110. continue;
  111. } catch (NotFoundHttpException $e) {
  112. $bar->advance();
  113. continue;
  114. } catch (\Exception $e) {
  115. $bar->advance();
  116. continue;
  117. }
  118. }
  119. $bar->finish();
  120. $this->line(' ');
  121. $this->info('Finished!');
  122. if($totalSize) {
  123. $this->info('Cleared ' . $totalSize . ' bytes of media from local disk!');
  124. }
  125. return 0;
  126. }
  127. protected function hugeMode($minId, $limit, $log)
  128. {
  129. $cloudDisk = Storage::disk(config('filesystems.cloud'));
  130. $localDisk = Storage::disk('local');
  131. $bar = $this->output->createProgressBar($limit);
  132. $bar->start();
  133. Media::whereRemoteMedia(false)
  134. ->whereNotNull(['status_id', 'cdn_url', 'replicated_at'])
  135. ->whereNot('version', '4')
  136. ->where('id', '<', $minId)
  137. ->chunk(50, function($medias) use($cloudDisk, $localDisk, $bar, $log) {
  138. foreach($medias as $media) {
  139. try {
  140. if($cloudDisk->exists($media->media_path)) {
  141. if( $localDisk->exists($media->media_path)) {
  142. $localDisk->delete($media->media_path);
  143. $media->version = 4;
  144. $media->save();
  145. MediaService::del($media->status_id);
  146. StatusService::del($media->status_id, false);
  147. if($localDisk->exists($media->thumbnail_path)) {
  148. $localDisk->delete($media->thumbnail_path);
  149. }
  150. } else {
  151. $media->version = 4;
  152. $media->save();
  153. }
  154. } else {
  155. if($log) {
  156. Log::channel('media')->info('[GC] Local media not properly persisted to cloud storage', ['media_id' => $media->id]);
  157. }
  158. }
  159. $bar->advance();
  160. } catch (FileNotFoundException $e) {
  161. $bar->advance();
  162. continue;
  163. } catch (NotFoundHttpException $e) {
  164. $bar->advance();
  165. continue;
  166. } catch (\Exception $e) {
  167. $bar->advance();
  168. continue;
  169. }
  170. }
  171. });
  172. $bar->finish();
  173. $this->line(' ');
  174. $this->info('Finished!');
  175. }
  176. }