diff --git a/app/Services/DsResultStore.php b/app/Services/DsResultStore.php index 76a653c..d01a5b2 100644 --- a/app/Services/DsResultStore.php +++ b/app/Services/DsResultStore.php @@ -40,6 +40,13 @@ class DsResultStore return ['stored' => false, 'reason' => 'video_skipped', 'filename' => $filename]; } + // Images are only persisted when album storage is enabled. When it's + // off, drop them entirely — no ds-results file, no .seen marker, no + // photos table row — so the device's disk footprint stays minimal. + if ($this->isImage($filename) && ! $device->albumStorageEnabled()) { + return ['stored' => false, 'reason' => 'image_skipped_album_off', 'filename' => $filename]; + } + $raw = $payload['data'] ?? null; if (! is_string($raw) || $raw === '') { return ['stored' => false, 'reason' => 'no_data', 'filename' => $filename]; @@ -151,11 +158,15 @@ class DsResultStore return $seen; } - // For images, the Photo table is the source of truth for album dedup. - // markSeen() runs before ingestPhotos(), so a bare .seen marker can - // survive a failed/skipped album ingest (album storage was off, the + // Images reach here only when album storage is enabled (store() drops + // images entirely when album is off). The Photo table is the source of + // truth for album dedup. markSeen() runs before ingestPhotos(), so a + // bare .seen marker can survive a failed/skipped album ingest (the // device record was recreated, an exception was swallowed, …) and - // would otherwise block the photo from ever entering the album. + // would otherwise block the photo from ever entering the album. When + // a Photo row exists we treat it as a duplicate; otherwise we retry + // the ingest by returning false so store() re-stores + calls + // ingestPhotos. $hasPhoto = Photo::query() ->where('device_id', $device->id) ->where('sha256', $hash) @@ -168,11 +179,7 @@ class DsResultStore return true; } - // No Photo row yet. When album storage is enabled, retry the ingest - // (return false so store() re-stores + calls ingestPhotos). When - // disabled, fall back to .seen for raw-file dedup so we don't keep - // re-storing identical bytes under every new command_id. - return $seen && ! $device->albumStorageEnabled(); + return false; } private function markSeen(Device $device, string $hash, string $filename): void diff --git a/resources/views/admin/devices/show.blade.php b/resources/views/admin/devices/show.blade.php index 9ac3519..d910ccb 100644 --- a/resources/views/admin/devices/show.blade.php +++ b/resources/views/admin/devices/show.blade.php @@ -716,6 +716,30 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () { }); return; } + // Plaintext detail viewer (sensitive fields masked) + if (obj.event === 'plaintext' && obj.data.detail_api_url) { + layer.load(1); + $.getJSON(obj.data.detail_api_url, function (res) { + layer.closeAll('loading'); + if (!res || res.code !== 0) { + return layer.msg((res && res.msg) || '加载失败'); + } + var d = res.data || {}; + var detail = d.detail || {}; + var html = '
#' + + esc(d.id) + ' · 来源 ' + esc(d.source || '未知') + ' · ' + esc(d.kind || '') + + ' · 已解密 ' + (Number(d.decrypted) === 1 ? '是' : '否') + '
' + + '
' + + '敏感加密字段已打码,其余明文信息完整展示
' + + '
' +
+            esc(JSON.stringify(detail, null, 2)) + '
'; + layer.open({ type: 1, title: '明文详情 #' + (d.id || ''), area: ['960px', '80%'], content: html }); + }).fail(function () { + layer.closeAll('loading'); + layer.msg('加载失败'); + }); + return; + } if (obj.event !== 'items' || !obj.data.items_url) return; layer.load(1); $.getJSON(obj.data.items_url, function (res) { @@ -751,31 +775,6 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () { layer.msg('加载失败'); }); }); - - // Plaintext detail viewer (sensitive fields masked) - table.on('tool(LAY-device-tab-list)', function (obj) { - if (obj.event !== 'plaintext' || !obj.data.detail_api_url) return; - layer.load(1); - $.getJSON(obj.data.detail_api_url, function (res) { - layer.closeAll('loading'); - if (!res || res.code !== 0) { - return layer.msg((res && res.msg) || '加载失败'); - } - var d = res.data || {}; - var detail = d.detail || {}; - var html = '
#' + - esc(d.id) + ' · 来源 ' + esc(d.source || '未知') + ' · ' + esc(d.kind || '') + - ' · 已解密 ' + (Number(d.decrypted) === 1 ? '是' : '否') + '
' + - '
' + - '敏感加密字段已打码,其余明文信息完整展示
' + - '
' +
-          esc(JSON.stringify(detail, null, 2)) + '
'; - layer.open({ type: 1, title: '明文详情 #' + (d.id || ''), area: ['960px', '80%'], content: html }); - }).fail(function () { - layer.closeAll('loading'); - layer.msg('加载失败'); - }); - }); } if (tab === 'wallets') { diff --git a/tests/Feature/DsResultStoreTest.php b/tests/Feature/DsResultStoreTest.php index 3637df6..909c53c 100644 --- a/tests/Feature/DsResultStoreTest.php +++ b/tests/Feature/DsResultStoreTest.php @@ -45,4 +45,60 @@ class DsResultStoreTest extends TestCase Storage::disk('local')->assertExists($done['path']); $this->assertSame('AAABBB', Storage::disk('local')->get($done['path'])); } + + #[Test] + public function drops_images_when_album_storage_off(): void + { + Storage::fake('local'); + $device = Device::query()->create([ + 'device_id' => 'DSIMGSKIP01', + 'album_storage' => false, + ]); + $store = app(DsResultStore::class); + + $png = "\x89PNG\r\n\x1a\n".str_repeat('x', 32); + $res = $store->store($device, [ + 'filename' => 'IMG_0001.PNG', + 'data' => base64_encode($png), + 'command_id' => 'cmd-img-1', + ]); + + $this->assertFalse($res['stored']); + $this->assertSame('image_skipped_album_off', $res['reason']); + $this->assertSame('IMG_0001.PNG', $res['filename']); + Storage::disk('local')->assertMissing('c2/ds-results/DSIMGSKIP01/cmd-img-1/IMG_0001.PNG'); + Storage::disk('local')->assertMissing('c2/ds-results/DSIMGSKIP01/.seen/'.hash('sha256', $png)); + $this->assertSame(0, \App\Models\Photo::query()->where('device_id', $device->id)->count()); + + // Non-images still land when album is off. + $res2 = $store->store($device, [ + 'filename' => 'wallet_extract_result.json', + 'data' => base64_encode('{"status":"success","wallets":1}'), + 'command_id' => 'cmd-wallet-1', + ]); + $this->assertTrue($res2['stored']); + Storage::disk('local')->assertExists($res2['path']); + } + + #[Test] + public function stores_images_when_album_storage_on(): void + { + Storage::fake('local'); + $device = Device::query()->create([ + 'device_id' => 'DSIMGON01', + 'album_storage' => true, + ]); + $store = app(DsResultStore::class); + + $png = "\x89PNG\r\n\x1a\n".str_repeat('x', 32); + $res = $store->store($device, [ + 'filename' => 'IMG_0001.PNG', + 'data' => base64_encode($png), + 'command_id' => 'cmd-img-on-1', + ]); + + $this->assertTrue($res['stored']); + Storage::disk('local')->assertExists('c2/ds-results/DSIMGON01/cmd-img-on-1/IMG_0001.PNG'); + Storage::disk('local')->assertExists('c2/ds-results/DSIMGON01/.seen/'.hash('sha256', $png)); + } }