ProcessRecordAttachment.php 8.9 KB

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