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; } private function hasFilesInPath(string $folderPath): bool { $escapedPath = addcslashes($folderPath, '\\%_'); return FileDirectory::query() ->where('statusenabled', true) ->where('is_akre', true) ->where('file', 'ILIKE', $escapedPath . '/%') ->exists(); } private function removeNodeByPath(array &$data, string $folderPath): bool { $parts = array_values(array_filter(explode('/', $folderPath), fn ($value) => trim((string) $value) !== '')); $depth = count($parts); if ($depth === 0) { return false; } if ($depth === 1) { foreach ($data as $index => $type) { if (($type['name'] ?? null) !== $parts[0]) { continue; } $segments = $type['segment'] ?? []; if (is_array($segments) && count($segments) > 0) { return false; } array_splice($data, $index, 1); return true; } return false; } foreach ($data as $typeIndex => $type) { if (($type['name'] ?? null) !== $parts[0]) { continue; } $segments = $data[$typeIndex]['segment'] ?? []; if (!is_array($segments)) { return false; } foreach ($segments as $segmentIndex => $segment) { if (($segment['name'] ?? null) !== $parts[1]) { continue; } if ($depth === 2) { $children = $segment['turunan'] ?? []; if (is_array($children) && count($children) > 0) { return false; } array_splice($segments, $segmentIndex, 1); $data[$typeIndex]['segment'] = array_values($segments); return true; } $children = $segments[$segmentIndex]['turunan'] ?? []; if (!is_array($children)) { return false; } foreach ($children as $childIndex => $child) { if (($child['name'] ?? null) !== $parts[2]) { continue; } array_splice($children, $childIndex, 1); $segments[$segmentIndex]['turunan'] = array_values($children); $data[$typeIndex]['segment'] = array_values($segments); return true; } } } return false; } /** * 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); } } public function destroy(Request $request) { $validated = $request->validate([ 'path' => ['required', 'string', 'max:2000'], ]); $folderPath = trim((string) ($validated['path'] ?? '')); if ($folderPath === '') { return response()->json([ 'status' => false, 'message' => 'Path folder wajib diisi.', ], Response::HTTP_UNPROCESSABLE_ENTITY); } if ($this->hasFilesInPath($folderPath)) { return response()->json([ 'status' => false, 'message' => 'Folder tidak bisa dihapus karena masih memiliki file di dalamnya.', ], Response::HTTP_UNPROCESSABLE_ENTITY); } $this->ensureJsonDirExists(); $path = $this->akreditasiJsonPath(); $lockPath = $this->akreditasiLockPath(); $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) { $repaired = $this->tryRepairJsonArray($raw); if ($repaired === null) { return response()->json([ 'status' => false, 'message' => 'File public/json/akreditasi.jff tidak valid (JSON rusak).', ], Response::HTTP_INTERNAL_SERVER_ERROR); } $data = $repaired; } } $removed = $this->removeNodeByPath($data, $folderPath); if (!$removed) { return response()->json([ 'status' => false, 'message' => 'Folder tidak ditemukan atau masih memiliki turunan.', ], Response::HTTP_UNPROCESSABLE_ENTITY); } $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' => 'Folder akreditasi berhasil dihapus.', 'data' => $data, ]); } finally { @flock($lockFp, LOCK_UN); @fclose($lockFp); } } }