dir = rtrim($dir, '/\\'); } public function warnings(): array { return array_values(array_unique($this->warnings)); } public function begin(bool $drop): void { if (is_dir($this->dir)) { $empty = count(array_diff(scandir($this->dir) ?: [], ['.', '..'])) === 0; if (!$empty && !$this->overwrite && !$drop) throw new DbException("ディレクトリが空ではありません:{$this->dir}", 0, 'HY000'); } else if (!@mkdir($this->dir, 0755, true) && !is_dir($this->dir)) { throw new DbException("ディレクトリを作れません:{$this->dir}", 0, 'HY000'); } } private function path(string $rel): string { if (!Stores::safePath($rel)) throw new DbException("不正なパス:{$rel}", 0, 'HY000'); $p = $this->dir.'/'.$rel; $d = dirname($p); if (!is_dir($d) && !@mkdir($d, 0755, true) && !is_dir($d)) throw new DbException("ディレクトリを作れません:{$d}", 0, 'HY000'); return $p; } private function put(string $rel, string $data): void { $p = $this->path($rel); $tmp = $p.'.'.bin2hex(random_bytes(4)).'.tmp'; if (file_put_contents($tmp, $data) === false || !rename($tmp, $p)) { @unlink($tmp); throw new DbException("ファイルに書けません:{$p}", 0, 'HY000'); } } public function createTable(array $t): void { $l = strtolower($t['name']); if (isset(Stores::tables()[$l])) { $this->buf[$l] = []; return; } // 普通のテーブル:定義を書いて、行を追加していく $p = $this->path(Stores::tableFile($t['name'])); $fh = @fopen($p.'.tmp', 'wb'); if ($fh === false) throw new DbException("ファイルに書けません:{$p}", 0, 'HY000'); $head = ['table' => $t['name'], 'columns' => array_map(fn($c) => array_filter($c, fn($v) => $v !== null && $v !== false) + ['name' => $c['name'], 'type' => $c['type']], $t['columns']), 'pk' => $t['pk'], 'indexes' => $t['indexes']]; foreach ($head['columns'] as $i => $c) { if (isset($c['default']['value'])) $head['columns'][$i]['default']['value'] = self::jsonValue($c['default']['value']); } $json = json_encode($head, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); fwrite($fh, substr($json, 0, -1).",\"rows\":[\n"); $this->open[$l] = [$fh, $p]; $this->first[$l] = true; } /** JSONに入れる値(BLOBとNaN・無限) */ public static function jsonValue(mixed $v): mixed { if ($v instanceof Blob) return ['$blob' => base64_encode($v->data)]; if (is_float($v) && (is_nan($v) || is_infinite($v))) return ['$float' => is_nan($v) ? 'NaN' : ($v > 0 ? 'Infinity' : '-Infinity')]; return $v; } public function insertRows(array $t, array $rows): void { $l = strtolower($t['name']); if (isset($this->buf[$l])) { // 列の名前で(書く先で列の順番が違っても良い様に) foreach ($rows as $r) { $row = []; foreach ($t['columns'] as $i => $c) $row[strtolower($c['name'])] = $r[$i]; $this->buf[$l][] = $row; } return; } [$fh] = $this->open[$l]; $out = ''; foreach ($rows as $r) { $line = json_encode(array_map([self::class, 'jsonValue'], $r), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION | JSON_INVALID_UTF8_SUBSTITUTE); $out .= ($this->first[$l] ? '' : ",\n").$line; $this->first[$l] = false; } fwrite($fh, $out); } public function finishTable(array $t): void { $l = strtolower($t['name']); if (!isset($this->open[$l])) return; [$fh, $p] = $this->open[$l]; fwrite($fh, "\n]}\n"); fclose($fh); rename($p.'.tmp', $p); unset($this->open[$l]); } public function end(): void { $b = $this->buf; $col = fn(array $r, string $k) => $r[$k] ?? null; $decode = function(mixed $v, string $what) { if (is_array($v) || $v === null) return $v; $d = json_decode((string)$v, true); if (json_last_error() !== JSON_ERROR_NONE) throw new DbException("不正なJSON:{$what}", 0, 'HY000'); return $d; }; $seqSort = fn(array &$rows) => usort($rows, fn($a, $b) => (int)$a['seq'] <=> (int)$b['seq']); foreach ($b['lb_users'] ?? [] as $r) { $name = (string)$col($r, 'username'); if (!preg_match('/^[A-Za-z0-9]+$/', $name) || !is_numeric($col($r, 'id'))) throw new DbException("不正なユーザー:{$name}", 0, 'HY000'); $decode($r['data'], "lb_users.{$name}"); $this->put('user/'.(int)$r['id'].'.'.$name.'.json', (string)$r['data']); } $threads = []; foreach ($b['lb_comment_threads'] ?? [] as $r) $threads[(string)$r['thread']] = $decode($r['data'], 'lb_comment_threads') ?: []; $comments = []; foreach ($b['lb_comments'] ?? [] as $r) $comments[(string)$r['thread']][] = $r; foreach ($comments as $thread => $list) $threads[$thread] ??= []; foreach ($threads as $thread => $doc) { if (!Stores::safeThread($thread)) throw new DbException("不正なスレッド:{$thread}", 0, 'HY000'); $list = $comments[$thread] ?? []; $seqSort($list); $doc = is_array($doc) ? $doc : []; $doc['comments'] = []; foreach ($list as $r) $doc['comments'][(string)$r['id']] = $decode($r['data'], "lb_comments.{$thread}"); $this->put("comments/{$thread}.json", (string)json_encode($doc, Stores::JSON_FLAGS)); } if (!empty($b['lb_comment_ap'])) { $map = []; foreach ($b['lb_comment_ap'] as $r) $map[(string)$r['ap_id']] = $decode($r['data'], 'lb_comment_ap'); $this->put('comments/ap.json', (string)json_encode($map, Stores::JSON_FLAGS)); } $notes = []; foreach ($b['lb_notifications'] ?? [] as $r) $notes[(string)$r['username']][] = $r; foreach ($notes as $user => $list) { if (!Stores::safeName($user)) throw new DbException("不正なユーザー:{$user}", 0, 'HY000'); $seqSort($list); $this->put("notifications/{$user}.json", (string)json_encode(array_map(fn($r) => $decode($r['data'], 'lb_notifications'), $list), Stores::JSON_FLAGS)); } if (isset($b['lb_cron'])) { $list = $b['lb_cron']; $seqSort($list); $this->put('cron/cron.json', (string)json_encode(array_map(fn($r) => $decode($r['data'], 'lb_cron'), $list), Stores::JSON_FLAGS)); } foreach (['lb_followers' => 'followers.txt', 'lb_following' => 'following.txt'] as $t => $file) { if (!isset($b[$t])) continue; $list = $b[$t]; $seqSort($list); $lines = array_values(array_filter(array_map(fn($r) => trim((string)$r['actor']), $list), fn($l) => $l !== '')); $this->put($file, empty($lines) ? '' : implode("\n", $lines)."\n"); } foreach ($b['lb_ap_notes'] ?? [] as $r) { $id = (string)$r['id']; if (!Stores::safeName($id)) throw new DbException("不正なID:{$id}", 0, 'HY000'); $decode($r['data'], "lb_ap_notes.{$id}"); $this->put("ap/notes/{$id}.json", (string)$r['data']); } foreach ($b['lb_files'] ?? [] as $r) { $c = $r['content']; $this->put((string)$r['path'], $c instanceof Blob ? $c->data : (string)$c); } $this->buf = []; } public function abort(): void { foreach ($this->open as [$fh, $p]) { @fclose($fh); @unlink($p.'.tmp'); } $this->open = []; $this->buf = []; } }