ProcessRecordAttachment.php 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. <?php
  2. namespace App\Jobs;
  3. use Illuminate\Bus\Queueable;
  4. use Illuminate\Contracts\Queue\ShouldQueue;
  5. use Illuminate\Foundation\Bus\Dispatchable;
  6. use Illuminate\Queue\InteractsWithQueue;
  7. use Illuminate\Queue\SerializesModels;
  8. use Illuminate\Support\Facades\DB;
  9. use Illuminate\Support\Facades\Log;
  10. use Illuminate\Support\Facades\Storage;
  11. use Illuminate\Support\Str;
  12. use App\Services\RecordFileService;
  13. class ProcessRecordAttachment implements ShouldQueue
  14. {
  15. use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
  16. protected $recordId;
  17. protected $tempFilePath;
  18. protected $originalFileName;
  19. protected $type;
  20. protected $clientName;
  21. public $timeout = 300;
  22. public $tries = 3;
  23. public $backoff = [10, 30, 60];
  24. public function __construct($recordId, $tempFilePath, $originalFileName, $type = 'OUT', $clientName = null)
  25. {
  26. $this->recordId = $recordId;
  27. $this->tempFilePath = $tempFilePath;
  28. $this->originalFileName = $originalFileName;
  29. $this->type = strtolower($type);
  30. $this->clientName = $clientName ?: session('clientName', 'default');
  31. $this->clientName = Str::slug($this->clientName, '_');
  32. }
  33. public function handle(RecordFileService $recordFileService)
  34. {
  35. try {
  36. Log::info("=== PROCESSING ATTACHMENT JOB START ===");
  37. Log::info("Client: {$this->clientName}");
  38. Log::info("Record ID: {$this->recordId}");
  39. Log::info("Temp file: {$this->tempFilePath}");
  40. Log::info("Original name: {$this->originalFileName}");
  41. Log::info("Type: {$this->type}");
  42. DB::table('records')
  43. ->where('id', $this->recordId)
  44. ->update([
  45. 'attachment_status' => 'processing',
  46. 'updated_at' => now()
  47. ]);
  48. if (!Storage::disk('s3')->exists($this->tempFilePath)) {
  49. Log::error("Temp file not found on S3: {$this->tempFilePath}");
  50. try {
  51. $tempFiles = Storage::disk('s3')->files("{$this->clientName}/temp/uploads");
  52. Log::info("Available temp files for client '{$this->clientName}' on S3: " . json_encode($tempFiles));
  53. } catch (\Exception $e) {
  54. Log::error("Could not list temp files for client '{$this->clientName}': " . $e->getMessage());
  55. }
  56. throw new \Exception("Temp file not found on S3: {$this->tempFilePath}");
  57. }
  58. $tempFileSize = Storage::disk('s3')->size($this->tempFilePath);
  59. Log::info("Temp file size: {$tempFileSize} bytes");
  60. $extension = pathinfo($this->originalFileName, PATHINFO_EXTENSION);
  61. $fileName = time() . '_' . Str::random(10) . '.' . $extension;
  62. $finalPath = "{$this->clientName}/records/{$this->type}/{$this->recordId}/attachments/{$fileName}";
  63. Log::info("Final path: {$finalPath}");
  64. $copySuccess = $this->copyFileOnS3($this->tempFilePath, $finalPath);
  65. if (!$copySuccess) {
  66. throw new \Exception("Failed to copy file from {$this->tempFilePath} to {$finalPath}");
  67. }
  68. if (!Storage::disk('s3')->exists($finalPath)) {
  69. throw new \Exception("Final file not found after copy: {$finalPath}");
  70. }
  71. $finalFileSize = Storage::disk('s3')->size($finalPath);
  72. Log::info("Final file size: {$finalFileSize} bytes");
  73. if ($finalFileSize !== $tempFileSize) {
  74. Log::warning("File size mismatch! Temp: {$tempFileSize}, Final: {$finalFileSize}");
  75. } else {
  76. Log::info("File sizes match - copy successful");
  77. }
  78. DB::table('records')
  79. ->where('id', $this->recordId)
  80. ->update([
  81. 'attachment' => $finalPath,
  82. 'attachment_status' => 'completed',
  83. 'updated_at' => now()
  84. ]);
  85. $this->cleanupTempFile($this->tempFilePath);
  86. Log::info("Attachment processing completed successfully for record {$this->recordId}: {$finalPath}");
  87. Log::info("=== PROCESSING ATTACHMENT JOB END ===");
  88. } catch (\Exception $e) {
  89. Log::error("Failed to process attachment for record {$this->recordId}: " . $e->getMessage());
  90. Log::error("Stack trace: " . $e->getTraceAsString());
  91. DB::table('records')
  92. ->where('id', $this->recordId)
  93. ->update([
  94. 'attachment_status' => 'failed',
  95. 'updated_at' => now()
  96. ]);
  97. $this->cleanupTempFile($this->tempFilePath);
  98. throw $e;
  99. }
  100. }
  101. /**
  102. * Enhanced S3 copy with multiple fallback approaches
  103. */
  104. private function copyFileOnS3($sourcePath, $destinationPath)
  105. {
  106. Log::info("Attempting S3 copy from {$sourcePath} to {$destinationPath}");
  107. try {
  108. Log::info("Trying Method 1: Standard S3 copy");
  109. $copyResult = Storage::disk('s3')->copy($sourcePath, $destinationPath);
  110. if ($copyResult && Storage::disk('s3')->exists($destinationPath)) {
  111. Log::info("Method 1 successful: Standard S3 copy");
  112. return true;
  113. } else {
  114. Log::warning("Method 1 failed: Standard S3 copy returned " . ($copyResult ? 'true' : 'false'));
  115. }
  116. } catch (\Exception $e) {
  117. Log::warning("Method 1 exception: " . $e->getMessage());
  118. }
  119. try {
  120. Log::info("Trying Method 2: Read and write");
  121. $fileContent = Storage::disk('s3')->get($sourcePath);
  122. if (!$fileContent) {
  123. throw new \Exception("Could not read source file content");
  124. }
  125. $writeResult = Storage::disk('s3')->put($destinationPath, $fileContent);
  126. if ($writeResult && Storage::disk('s3')->exists($destinationPath)) {
  127. Log::info("Method 2 successful: Read and write");
  128. return true;
  129. } else {
  130. Log::warning("Method 2 failed: Write returned " . ($writeResult ? 'true' : 'false'));
  131. }
  132. } catch (\Exception $e) {
  133. Log::warning("Method 2 exception: " . $e->getMessage());
  134. }
  135. try {
  136. Log::info("Trying Method 3: Stream copy");
  137. $sourceStream = Storage::disk('s3')->readStream($sourcePath);
  138. if (!$sourceStream) {
  139. throw new \Exception("Could not open source stream");
  140. }
  141. $writeResult = Storage::disk('s3')->writeStream($destinationPath, $sourceStream);
  142. if (is_resource($sourceStream)) {
  143. fclose($sourceStream);
  144. }
  145. if ($writeResult && Storage::disk('s3')->exists($destinationPath)) {
  146. Log::info(" Method 3 successful: Stream copy");
  147. return true;
  148. } else {
  149. Log::warning("Method 3 failed: Stream write returned " . ($writeResult ? 'true' : 'false'));
  150. }
  151. } catch (\Exception $e) {
  152. Log::warning("Method 3 exception: " . $e->getMessage());
  153. }
  154. Log::error("All S3 copy methods failed");
  155. return false;
  156. }
  157. /**
  158. * Clean up temp file with error handling
  159. */
  160. private function cleanupTempFile($tempPath)
  161. {
  162. try {
  163. if (Storage::disk('s3')->exists($tempPath)) {
  164. $deleted = Storage::disk('s3')->delete($tempPath);
  165. if ($deleted) {
  166. Log::info("Temp file deleted: {$tempPath}");
  167. } else {
  168. Log::warning("Failed to delete temp file: {$tempPath}");
  169. }
  170. } else {
  171. Log::info("Temp file already gone: {$tempPath}");
  172. }
  173. } catch (\Exception $e) {
  174. Log::error("Error deleting temp file {$tempPath}: " . $e->getMessage());
  175. }
  176. }
  177. public function failed(\Exception $exception)
  178. {
  179. Log::error("=== JOB PERMANENTLY FAILED ===");
  180. Log::error("Client: {$this->clientName}");
  181. Log::error("Record ID: {$this->recordId}");
  182. Log::error("Exception: " . $exception->getMessage());
  183. DB::table('records')
  184. ->where('id', $this->recordId)
  185. ->update([
  186. 'attachment_status' => 'failed',
  187. 'updated_at' => now()
  188. ]);
  189. $this->cleanupTempFile($this->tempFilePath);
  190. }
  191. /**
  192. * Get job tags for monitoring
  193. */
  194. public function tags()
  195. {
  196. return [
  197. 'attachment',
  198. 'client:' . $this->clientName,
  199. 'record:' . $this->recordId,
  200. 'type:' . $this->type,
  201. 'file:' . basename($this->tempFilePath)
  202. ];
  203. }
  204. }