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));
+ }
}