• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LeTraceurSnorkLibrary / MessSaga / 23892487014

02 Apr 2026 08:54AM UTC coverage: 29.947% (-1.7%) from 31.674%
23892487014

Pull #15

github

web-flow
Merge c1433e08b into a7d6f3637
Pull Request #15: feat: add s3 as storage

30 of 155 new or added lines in 14 files covered. (19.35%)

4 existing lines in 4 files now uncovered.

448 of 1496 relevant lines covered (29.95%)

0.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/app/Jobs/ProcessChatImport.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace App\Jobs;
6

7
use App\Services\Import\Archives\DTO\ArchiveExtractionResult;
8
use App\Services\Import\Archives\Exceptions\ArchiveExtractionFailedException;
9
use App\Services\Import\Factories\ImportArchiveExtractorFactory;
10
use App\Services\Import\Strategies\ImportOnlyMediaFilesStrategyInterface;
11
use App\Services\Import\Strategies\ImportStrategyInterface;
12
use App\Services\ImportService;
13
use App\Services\Media\ImportedMediaResolverService;
14
use App\Services\Media\Storage\MediaStorageInterface;
15
use App\Services\Parsers\ParserRegistry;
16
use Illuminate\Bus\Queueable;
17
use Illuminate\Contracts\Queue\ShouldQueue;
18
use Illuminate\Database\QueryException;
19
use Illuminate\Foundation\Bus\Dispatchable;
20
use Illuminate\Queue\InteractsWithQueue;
21
use Illuminate\Queue\SerializesModels;
22
use Illuminate\Support\Facades\Log;
23
use Illuminate\Support\Facades\Storage;
24

25
class ProcessChatImport implements ShouldQueue
26
{
27
    use Dispatchable;
28
    use InteractsWithQueue;
29
    use Queueable;
30
    use SerializesModels;
31

32
    /**
33
     * @param int                     $userId
34
     * @param string                  $messengerType
35
     * @param string                  $exportFileStoredPath Storage-relative path (archive or single export file)
36
     * @param ImportStrategyInterface $strategy
37
     */
38
    public function __construct(
39
        public int                     $userId,
40
        public string                  $messengerType,
41
        public string                  $exportFileStoredPath,
42
        public ImportStrategyInterface $strategy
43
    ) {
44
    }
×
45

46
    /**
47
     * @param ImportArchiveExtractorFactory $archiveExtractorsFactory
48
     * @param ParserRegistry                $parserRegistry
49
     * @param ImportedMediaResolverService  $importedMediaResolverService
50
     * @param MediaStorageInterface         $mediaStorage
51
     *
52
     * @return void
53
     */
54
    public function handle(
55
        ImportArchiveExtractorFactory $archiveExtractorsFactory,
56
        ParserRegistry                $parserRegistry,
57
        ImportedMediaResolverService  $importedMediaResolverService,
58
        MediaStorageInterface         $mediaStorage
59
    ): void {
NEW
60
        $importsTmpDiskName = (string)config('filesystems.imports_tmp_disk', 'imports_tmp');
×
NEW
61
        $importsTmpDisk     = Storage::disk($importsTmpDiskName);
×
62
        $source             = null;
×
63
        $extractedDir       = null;
×
64
        $shouldDeleteSource = true;
×
65

66
        try {
67
            $archiveExtractor = $archiveExtractorsFactory->makeForPath($this->exportFileStoredPath);
×
68
            if ($archiveExtractor !== null) {
×
69
                $source = $archiveExtractor->extract($this->exportFileStoredPath, $this->messengerType);
×
70

71
                $extractedDir = $source->getExtractedDir();
×
72

73
                /**
74
                 * If export file (e.g. result.json) is not presented - we cannot perform import
75
                 */
76
                if ($source->getExportFileAbsolutePath() === null) {
×
77
                    /**
78
                     * Though, if import mode is 'To selected conversation' - we can try to import media to already
79
                     * existing conversation
80
                     */
81
                    $this->runMediaOnlyFallback(
×
82
                        archiveExtractorsFactory: $archiveExtractorsFactory,
×
83
                        parserRegistry: $parserRegistry,
×
NEW
84
                        importedMediaResolverService: $importedMediaResolverService,
×
NEW
85
                        mediaStorage: $mediaStorage
×
UNCOV
86
                    );
×
87

88
                    return;
×
89
                }
90
            }
91
            $extractedExportFile = $source ?? new ArchiveExtractionResult(
×
NEW
92
                $importsTmpDisk->path($this->exportFileStoredPath),
×
93
                null,
×
94
                null
×
95
            );
×
96

97
            /**
98
             * @var ImportService $service
99
             */
100
            $service = app(ImportService::class);
×
101

102
            /**
103
             * Импорт читает файл экспорта из $extractedExportFile и при наличии медиа копирует их из распакованной
104
             * папки в постоянное хранилище (Storage).
105
             * К моменту выхода из import() файлы уже лежат в conversations/{id}/media/.
106
             */
107
            $service->import(
×
108
                userId: $this->userId,
×
109
                messengerType: $this->messengerType,
×
110
                strategy: $this->strategy,
×
111
                extractedExportFile: $extractedExportFile
×
112
            );
×
113
        } catch (ArchiveExtractionFailedException $e) {
×
114
            Log::warning('Archive extraction failed', [
×
115
                'user_id'          => $this->userId,
×
116
                'messenger_type'   => $this->messengerType,
×
117
                'export_file_path' => $this->exportFileStoredPath,
×
118
                'reason'           => $e->getMessage(),
×
119
            ]);
×
120
        } catch (QueryException $e) {
×
121
            Log::error('Import failed due to database query error', [
×
122
                'user_id'          => $this->userId,
×
123
                'messenger_type'   => $this->messengerType,
×
124
                'export_file_path' => $this->exportFileStoredPath,
×
125
                'strategy'         => $this->strategy->getName(),
×
126
                'error'            => $e->getMessage(),
×
127
            ]);
×
128

129
            $shouldDeleteSource = false;
×
130
            throw $e;
×
131
        } finally {
132
            /**
133
             * Delete temporary archive/export file
134
             */
NEW
135
            if ($shouldDeleteSource && $importsTmpDisk->exists($this->exportFileStoredPath)) {
×
NEW
136
                $importsTmpDisk->delete($this->exportFileStoredPath);
×
137
            }
138

139
            /**
140
             * Delete extraction directory
141
             */
NEW
142
            if (isset($extractedDir) && $importsTmpDisk->exists($extractedDir)) {
×
NEW
143
                $importsTmpDisk->deleteDirectory($extractedDir);
×
144
            }
145
        }
146
    }
147

148
    /**
149
     * Fallback for "Import to selected conversation" option when archive has additional media files that should be
150
     * merged into selected conversation
151
     *
152
     * @param ImportArchiveExtractorFactory $archiveExtractorsFactory
153
     * @param ParserRegistry                $parserRegistry
154
     * @param ImportedMediaResolverService  $importedMediaResolverService
155
     * @param MediaStorageInterface         $mediaStorage
156
     *
157
     * @return void
158
     */
159
    private function runMediaOnlyFallback(
160
        ImportArchiveExtractorFactory $archiveExtractorsFactory,
161
        ParserRegistry                $parserRegistry,
162
        ImportedMediaResolverService  $importedMediaResolverService,
163
        MediaStorageInterface         $mediaStorage
164
    ): void {
165
        /**
166
         * This fallback is only for "Import to selected conversation" scenario
167
         */
168
        if (!$this->strategy instanceof ImportOnlyMediaFilesStrategyInterface) {
×
169
            return;
×
170
        }
171

172
        $targetConversationId = $this->strategy->getImportMode()?->getTargetConversationId();
×
173
        if (!isset($targetConversationId)) {
×
174
            return;
×
175
        }
176

177
        $mediaUploadJob = new ProcessConversationMediaUpload(
×
178
            userId: $this->userId,
×
179
            conversationId: $targetConversationId,
×
180
            path: $this->exportFileStoredPath
×
181
        );
×
182

183
        $mediaUploadJob->handle(
×
184
            parserRegistry: $parserRegistry,
×
NEW
185
            importedMediaResolverService: $importedMediaResolverService,
×
NEW
186
            mediaStorage: $mediaStorage,
×
187
            archiveExtractorsFactory: $archiveExtractorsFactory
×
188
        );
×
189
    }
190
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc