317 lines
10 KiB
PHP
317 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
|
|
class AkreditasiInstrumenController extends Controller
|
|
{
|
|
private function akreditasiJsonPath(): string
|
|
{
|
|
return public_path('json/akreditasi.jff');
|
|
}
|
|
|
|
private function akreditasiLockPath(): string
|
|
{
|
|
return public_path('json/akreditasi.jff.lock');
|
|
}
|
|
|
|
private function ensureJsonDirExists(): void
|
|
{
|
|
$dir = public_path('json');
|
|
if (!is_dir($dir)) {
|
|
@mkdir($dir, 0777, true);
|
|
}
|
|
}
|
|
|
|
private function atomicWrite(string $path, string $contents): bool
|
|
{
|
|
$dir = dirname($path);
|
|
if (!is_dir($dir)) {
|
|
@mkdir($dir, 0777, true);
|
|
}
|
|
|
|
$tmp = $path . '.tmp';
|
|
$bytes = @file_put_contents($tmp, $contents);
|
|
if ($bytes === false || $bytes < strlen($contents)) {
|
|
@unlink($tmp);
|
|
return false;
|
|
}
|
|
|
|
// best-effort backup
|
|
if (file_exists($path)) {
|
|
@copy($path, $path . '.bak');
|
|
}
|
|
|
|
// replace
|
|
@unlink($path);
|
|
if (@rename($tmp, $path)) {
|
|
return true;
|
|
}
|
|
|
|
// fallback copy
|
|
$copied = @copy($tmp, $path);
|
|
@unlink($tmp);
|
|
return (bool) $copied;
|
|
}
|
|
|
|
private function stripUtf8Bom(string $raw): string
|
|
{
|
|
// Remove UTF-8 BOM if present
|
|
if (strncmp($raw, "\xEF\xBB\xBF", 3) === 0) {
|
|
return substr($raw, 3);
|
|
}
|
|
return $raw;
|
|
}
|
|
|
|
private function decodeJsonArray(string $raw, bool $strict = false): ?array
|
|
{
|
|
$raw = $this->stripUtf8Bom($raw);
|
|
$data = json_decode($raw, true);
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
return $strict ? null : [];
|
|
}
|
|
return is_array($data) ? $data : ($strict ? null : []);
|
|
}
|
|
|
|
private function tryRepairJsonArray(string $raw): ?array
|
|
{
|
|
// Common corruption: trailing commas before ']' or '}'
|
|
$raw = $this->stripUtf8Bom($raw);
|
|
$repaired = preg_replace('/,(\s*[}\]])/m', '$1', $raw);
|
|
if (!is_string($repaired) || $repaired === '') {
|
|
return null;
|
|
}
|
|
$data = json_decode($repaired, true);
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
return null;
|
|
}
|
|
return is_array($data) ? $data : null;
|
|
}
|
|
|
|
private function splitList(?string $value): array
|
|
{
|
|
$value = trim((string) $value);
|
|
if ($value === '') return [];
|
|
|
|
$parts = preg_split('/[,\r\n]+/', $value) ?: [];
|
|
$out = [];
|
|
foreach ($parts as $p) {
|
|
$p = trim($p);
|
|
if ($p !== '') $out[] = $p;
|
|
}
|
|
return array_values(array_unique($out));
|
|
}
|
|
|
|
private function ensureTypeExists(array &$data, string $typeName): int
|
|
{
|
|
foreach ($data as $i => $t) {
|
|
if (($t['name'] ?? null) === $typeName) {
|
|
if (!isset($data[$i]['segment']) || !is_array($data[$i]['segment'])) {
|
|
$data[$i]['segment'] = [];
|
|
}
|
|
return $i;
|
|
}
|
|
}
|
|
$data[] = [
|
|
'name' => $typeName,
|
|
'segment' => [],
|
|
];
|
|
return count($data) - 1;
|
|
}
|
|
|
|
private function ensureSegmentExists(array &$typeSegments, string $segmentName): int
|
|
{
|
|
foreach ($typeSegments as $j => $s) {
|
|
if (($s['name'] ?? null) === $segmentName) {
|
|
if (!isset($typeSegments[$j]['turunan']) || !is_array($typeSegments[$j]['turunan'])) {
|
|
$typeSegments[$j]['turunan'] = [];
|
|
}
|
|
return $j;
|
|
}
|
|
}
|
|
$typeSegments[] = [
|
|
'name' => $segmentName,
|
|
'turunan' => [],
|
|
];
|
|
return count($typeSegments) - 1;
|
|
}
|
|
|
|
private function ensureItemExists(array &$segmentChildren, string $itemName): bool
|
|
{
|
|
foreach ($segmentChildren as $c) {
|
|
if (($c['name'] ?? null) === $itemName) {
|
|
return false; // already exists
|
|
}
|
|
}
|
|
$segmentChildren[] = ['name' => $itemName];
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Read akreditasi.jff (array)
|
|
*/
|
|
public function index()
|
|
{
|
|
$path = $this->akreditasiJsonPath();
|
|
if (!file_exists($path)) {
|
|
return response()->json([]);
|
|
}
|
|
|
|
$raw = @file_get_contents($path);
|
|
$data = $this->decodeJsonArray($raw === false ? '[]' : $raw, false) ?? [];
|
|
return response()->json($data);
|
|
}
|
|
|
|
/**
|
|
* Add "folder" structure to akreditasi.jff.
|
|
*
|
|
* Payload:
|
|
* - type (required)
|
|
* - segment (optional)
|
|
* - item (optional)
|
|
*
|
|
* Behavior:
|
|
* - type only: creates new top-level type {name, segment: []}
|
|
* - type + segment: creates segment {name, turunan: []} under type
|
|
* - type + segment + item: creates item {name} under segment.turunan
|
|
*/
|
|
public function store(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'type' => ['required', 'string', 'max:255'],
|
|
'segment' => ['nullable', 'string', 'max:255'],
|
|
'item' => ['nullable', 'string', 'max:2000'],
|
|
]);
|
|
|
|
$typeName = trim($validated['type'] ?? '');
|
|
$segmentRaw = isset($validated['segment']) ? (string) $validated['segment'] : '';
|
|
$itemRaw = isset($validated['item']) ? (string) $validated['item'] : '';
|
|
|
|
$segments = $this->splitList($segmentRaw);
|
|
$items = $this->splitList($itemRaw);
|
|
|
|
if ($typeName === '') {
|
|
return response()->json([
|
|
'status' => false,
|
|
'message' => 'Type wajib diisi.',
|
|
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
|
}
|
|
|
|
if (count($items) > 0 && count($segments) === 0) {
|
|
return response()->json([
|
|
'status' => false,
|
|
'message' => 'Segment wajib diisi jika ingin menambah item.',
|
|
], Response::HTTP_UNPROCESSABLE_ENTITY);
|
|
}
|
|
|
|
$this->ensureJsonDirExists();
|
|
$path = $this->akreditasiJsonPath();
|
|
$lockPath = $this->akreditasiLockPath();
|
|
|
|
// Use separate lock file so Windows can replace the target JSON file safely.
|
|
$lockFp = @fopen($lockPath, 'c+');
|
|
if (!$lockFp) {
|
|
return response()->json([
|
|
'status' => false,
|
|
'message' => 'Gagal membuka file lock untuk akreditasi.jff.',
|
|
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
|
}
|
|
|
|
try {
|
|
if (!flock($lockFp, LOCK_EX)) {
|
|
return response()->json([
|
|
'status' => false,
|
|
'message' => 'Gagal mengunci file akreditasi.jff.',
|
|
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
|
}
|
|
|
|
$raw = @file_get_contents($path);
|
|
if ($raw === false) {
|
|
$data = [];
|
|
} else {
|
|
$data = $this->decodeJsonArray($raw, true);
|
|
if ($data === null) {
|
|
// Try safe repair (e.g., remove trailing commas). If still invalid, abort to avoid data loss.
|
|
$repaired = $this->tryRepairJsonArray($raw);
|
|
if ($repaired === null) {
|
|
return response()->json([
|
|
'status' => false,
|
|
'message' => 'File public/json/akreditasi.jff tidak valid (JSON rusak). Perubahan dibatalkan agar data tidak hilang. Silakan restore dari akreditasi.jff.bak lalu coba lagi.',
|
|
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
|
}
|
|
$data = $repaired;
|
|
}
|
|
}
|
|
|
|
$typeIndex = $this->ensureTypeExists($data, $typeName);
|
|
|
|
$added = 0;
|
|
$skipped = 0;
|
|
|
|
// Case: type only
|
|
if (count($segments) === 0) {
|
|
// type already ensured; treat as skipped if it existed, added if new.
|
|
// We can't easily detect "new" without extra flag; keep message generic.
|
|
} elseif (count($items) === 0) {
|
|
// Case: add one or more segments
|
|
$typeSegments = $data[$typeIndex]['segment'];
|
|
foreach ($segments as $segName) {
|
|
$beforeCount = count($typeSegments);
|
|
$this->ensureSegmentExists($typeSegments, $segName);
|
|
$afterCount = count($typeSegments);
|
|
if ($afterCount > $beforeCount) $added++;
|
|
else $skipped++;
|
|
}
|
|
$data[$typeIndex]['segment'] = $typeSegments;
|
|
} else {
|
|
// Case: add one or more items under the given segment(s)
|
|
// If user provides multiple segments + items, add all items to each segment.
|
|
$typeSegments = $data[$typeIndex]['segment'];
|
|
foreach ($segments as $segName) {
|
|
$segIndex = $this->ensureSegmentExists($typeSegments, $segName);
|
|
$children = $typeSegments[$segIndex]['turunan'] ?? [];
|
|
if (!is_array($children)) $children = [];
|
|
|
|
foreach ($items as $itemName) {
|
|
$didAdd = $this->ensureItemExists($children, $itemName);
|
|
if ($didAdd) $added++;
|
|
else $skipped++;
|
|
}
|
|
$typeSegments[$segIndex]['turunan'] = $children;
|
|
}
|
|
$data[$typeIndex]['segment'] = $typeSegments;
|
|
}
|
|
|
|
// write back
|
|
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
if ($json === false) {
|
|
return response()->json([
|
|
'status' => false,
|
|
'message' => 'Gagal mengubah data menjadi JSON.',
|
|
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
|
}
|
|
$json .= "\n";
|
|
|
|
if (!$this->atomicWrite($path, $json)) {
|
|
return response()->json([
|
|
'status' => false,
|
|
'message' => 'Gagal menulis file akreditasi.jff (atomic write).',
|
|
], Response::HTTP_INTERNAL_SERVER_ERROR);
|
|
}
|
|
|
|
return response()->json([
|
|
'status' => true,
|
|
'message' => ($added > 0 || $skipped > 0)
|
|
? "Selesai. Ditambahkan: {$added}, dilewati (sudah ada): {$skipped}."
|
|
: 'Selesai.',
|
|
'data' => $data,
|
|
]);
|
|
} finally {
|
|
@flock($lockFp, LOCK_UN);
|
|
@fclose($lockFp);
|
|
}
|
|
}
|
|
}
|