1
0

StoryRotateMedia.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. <?php
  2. namespace App\Jobs\StoryPipeline;
  3. use App\Services\StoryIndexService;
  4. use App\Story;
  5. use Exception;
  6. use Illuminate\Bus\Queueable;
  7. use Illuminate\Contracts\Queue\ShouldQueue;
  8. use Illuminate\Foundation\Bus\Dispatchable;
  9. use Illuminate\Queue\InteractsWithQueue;
  10. use Illuminate\Queue\SerializesModels;
  11. use Illuminate\Support\Facades\DB;
  12. use Illuminate\Support\Facades\Log;
  13. use Illuminate\Support\Facades\Storage;
  14. use Illuminate\Support\Str;
  15. use Throwable;
  16. class StoryRotateMedia implements ShouldQueue
  17. {
  18. use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
  19. protected $story;
  20. protected $newPath;
  21. protected $oldPath;
  22. /**
  23. * The number of times the job may be attempted.
  24. */
  25. public $tries = 3;
  26. /**
  27. * The maximum number of seconds the job can run.
  28. */
  29. public $timeout = 300;
  30. /**
  31. * Calculate the number of seconds to wait before retrying the job.
  32. */
  33. public function backoff(): array
  34. {
  35. return [30, 60, 120];
  36. }
  37. /**
  38. * Create a new job instance.
  39. *
  40. * @return void
  41. */
  42. public function __construct(Story $story)
  43. {
  44. $this->story = $story;
  45. }
  46. /**
  47. * Execute the job.
  48. *
  49. * @return void
  50. */
  51. public function handle()
  52. {
  53. try {
  54. $story = $this->story->fresh();
  55. if (! $story) {
  56. if (config('app.dev_log')) {
  57. Log::warning('StoryRotateMedia: Story not found', ['story_id' => $this->story->id]);
  58. }
  59. return;
  60. }
  61. if ($story->local == false) {
  62. return;
  63. }
  64. $this->oldPath = $story->path;
  65. $this->newPath = $this->generateNewPath($this->oldPath);
  66. if (! Storage::exists($this->oldPath)) {
  67. if (config('app.dev_log')) {
  68. Log::warning('StoryRotateMedia: Original file not found', [
  69. 'story_id' => $story->id,
  70. 'path' => $this->oldPath,
  71. ]);
  72. }
  73. return;
  74. }
  75. $this->rotateMedia($story);
  76. if (config('app.dev_log')) {
  77. Log::info('StoryRotateMedia: Successfully rotated media', [
  78. 'story_id' => $story->id,
  79. 'old_path' => $this->oldPath,
  80. 'new_path' => $this->newPath,
  81. ]);
  82. }
  83. } catch (Exception $e) {
  84. if (config('app.dev_log')) {
  85. Log::error('StoryRotateMedia: Job failed', [
  86. 'story_id' => $this->story->id,
  87. 'error' => $e->getMessage(),
  88. 'trace' => $e->getTraceAsString(),
  89. ]);
  90. }
  91. throw $e;
  92. }
  93. }
  94. /**
  95. * Handle the media rotation process
  96. */
  97. protected function rotateMedia(Story $story)
  98. {
  99. DB::transaction(function () use ($story) {
  100. if (! Storage::copy($this->oldPath, $this->newPath)) {
  101. throw new Exception("Failed to copy file from {$this->oldPath} to {$this->newPath}");
  102. }
  103. if (! Storage::exists($this->newPath)) {
  104. throw new Exception("New file was not created at {$this->newPath}");
  105. }
  106. $story->path = $this->newPath;
  107. $story->bearcap_token = null;
  108. if (! $story->save()) {
  109. throw new Exception('Failed to update story record in database');
  110. }
  111. if (! Storage::delete($this->oldPath)) {
  112. if (config('app.dev_log')) {
  113. Log::warning('StoryRotateMedia: Failed to delete old file', [
  114. 'story_id' => $story->id,
  115. 'path' => $this->oldPath,
  116. ]);
  117. }
  118. }
  119. });
  120. $this->updateSearchIndex($story);
  121. }
  122. /**
  123. * Update the search index
  124. */
  125. protected function updateSearchIndex(Story $story)
  126. {
  127. try {
  128. $index = app(StoryIndexService::class);
  129. $index->removeStory($story->id, $story->profile_id);
  130. usleep(random_int(100000, 500000));
  131. $index->indexStory($story);
  132. } catch (Exception $e) {
  133. if (config('app.dev_log')) {
  134. Log::error('StoryRotateMedia: Failed to update search index', [
  135. 'story_id' => $story->id,
  136. 'error' => $e->getMessage(),
  137. ]);
  138. }
  139. }
  140. }
  141. /**
  142. * Generate a new path for the rotated media
  143. */
  144. protected function generateNewPath(string $oldPath): string
  145. {
  146. $paths = explode('/', $oldPath);
  147. $name = array_pop($paths);
  148. $ext = pathinfo($name, PATHINFO_EXTENSION);
  149. $new = Str::random(13).'_'.Str::random(24).'_'.Str::random(3).'.'.$ext;
  150. array_push($paths, $new);
  151. return implode('/', $paths);
  152. }
  153. /**
  154. * Handle a job failure.
  155. */
  156. public function failed(Throwable $exception)
  157. {
  158. if (config('app.dev_log')) {
  159. Log::error('StoryRotateMedia: Job permanently failed', [
  160. 'story_id' => $this->story->id,
  161. 'error' => $exception->getMessage(),
  162. 'attempts' => $this->attempts(),
  163. ]);
  164. }
  165. if ($this->newPath && Storage::exists($this->newPath)) {
  166. try {
  167. Storage::delete($this->newPath);
  168. if (config('app.dev_log')) {
  169. Log::info('StoryRotateMedia: Cleaned up orphaned file', [
  170. 'path' => $this->newPath,
  171. ]);
  172. }
  173. } catch (Exception $e) {
  174. if (config('app.dev_log')) {
  175. Log::error('StoryRotateMedia: Failed to cleanup orphaned file', [
  176. 'path' => $this->newPath,
  177. 'error' => $e->getMessage(),
  178. ]);
  179. }
  180. }
  181. }
  182. }
  183. /**
  184. * Determine the time at which the job should timeout.
  185. */
  186. public function retryUntil()
  187. {
  188. return now()->addHours(2);
  189. }
  190. }