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