PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]); } return $pdo; } // ---- Repositories ----------------------------------------------------------- /** * Sort options for the repository list, keyed by the value used in the URL. * Each value is a fixed ORDER BY clause (never interpolate user input directly). */ const REPO_SORTS = [ 'newest' => 'created_at DESC, id DESC', 'oldest' => 'created_at ASC, id ASC', 'name' => 'name ASC, id ASC', 'slug' => 'slug ASC', ]; function get_repositories(string $sort = 'newest'): array { $order = REPO_SORTS[$sort] ?? REPO_SORTS['newest']; $stmt = db()->query('SELECT * FROM repositories ORDER BY ' . $order); return $stmt->fetchAll(); } function get_repository_by_slug(string $slug): ?array { $stmt = db()->prepare('SELECT * FROM repositories WHERE slug = ? LIMIT 1'); $stmt->execute([$slug]); $row = $stmt->fetch(); return $row ?: null; } function get_repository(int $id): ?array { $stmt = db()->prepare('SELECT * FROM repositories WHERE id = ? LIMIT 1'); $stmt->execute([$id]); $row = $stmt->fetch(); return $row ?: null; } function create_repository(string $slug, string $name, string $desc, string $lang): int { $stmt = db()->prepare( 'INSERT INTO repositories (slug, name, description, language) VALUES (?, ?, ?, ?)' ); $stmt->execute([$slug, $name, $desc, $lang]); return (int) db()->lastInsertId(); } function update_repository(int $id, string $name, string $desc, string $lang): void { $stmt = db()->prepare( 'UPDATE repositories SET name = ?, description = ?, language = ? WHERE id = ?' ); $stmt->execute([$name, $desc, $lang, $id]); } /** * Rename a repository's slug. Updates the repositories row and rewrites the * stored slug prefix on every file's `filepath` (which is "slug/rel-path"). * Runs in a transaction so both tables stay consistent. The caller is * responsible for renaming the on-disk uploads/ directory. */ function rename_repository(int $id, string $oldSlug, string $newSlug): void { $pdo = db(); $pdo->beginTransaction(); try { $stmt = $pdo->prepare('UPDATE repositories SET slug = ? WHERE id = ?'); $stmt->execute([$newSlug, $id]); // filepath looks like "oldslug/dir/file.ext" — swap the leading segment. $stmt = $pdo->prepare( 'UPDATE files SET filepath = CONCAT(?, SUBSTRING(filepath, ?)) WHERE repo_id = ?' ); $stmt->execute([$newSlug, strlen($oldSlug) + 1, $id]); $pdo->commit(); } catch (Throwable $e) { $pdo->rollBack(); throw $e; } } function delete_repository(int $id): void { // Files rows are removed automatically via ON DELETE CASCADE. $stmt = db()->prepare('DELETE FROM repositories WHERE id = ?'); $stmt->execute([$id]); } // ---- Files ------------------------------------------------------------------ function get_files_by_repo(int $repoId): array { $stmt = db()->prepare('SELECT * FROM files WHERE repo_id = ? ORDER BY filename ASC'); $stmt->execute([$repoId]); return $stmt->fetchAll(); } function get_file_by_name(int $repoId, string $filename): ?array { $stmt = db()->prepare('SELECT * FROM files WHERE repo_id = ? AND filename = ? LIMIT 1'); $stmt->execute([$repoId, $filename]); $row = $stmt->fetch(); return $row ?: null; } function get_file(int $id): ?array { $stmt = db()->prepare('SELECT * FROM files WHERE id = ? LIMIT 1'); $stmt->execute([$id]); $row = $stmt->fetch(); return $row ?: null; } function create_file(int $repoId, string $filename, string $filepath, int $size): int { // Replace an existing same-named file record for this repo (re-upload). $stmt = db()->prepare( 'INSERT INTO files (repo_id, filename, filepath, filesize) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE filepath = VALUES(filepath), filesize = VALUES(filesize)' ); $stmt->execute([$repoId, $filename, $filepath, $size]); return (int) db()->lastInsertId(); } function delete_file(int $id): void { $stmt = db()->prepare('DELETE FROM files WHERE id = ?'); $stmt->execute([$id]); } // ---- Folders ---------------------------------------------------------------- function get_folders_by_repo(int $repoId): array { $stmt = db()->prepare('SELECT * FROM folders WHERE repo_id = ? ORDER BY path ASC'); $stmt->execute([$repoId]); return $stmt->fetchAll(); } function get_folder(int $id): ?array { $stmt = db()->prepare('SELECT * FROM folders WHERE id = ? LIMIT 1'); $stmt->execute([$id]); $row = $stmt->fetch(); return $row ?: null; } /** * Register a folder path and all of its ancestors. For "a/b/c" this inserts * "a", "a/b" and "a/b/c". Existing rows are ignored (INSERT IGNORE). */ function create_folder(int $repoId, string $path): void { $stmt = db()->prepare('INSERT IGNORE INTO folders (repo_id, path) VALUES (?, ?)'); $accum = []; foreach (explode('/', trim($path, '/')) as $seg) { if ($seg === '') { continue; } $accum[] = $seg; $stmt->execute([$repoId, implode('/', $accum)]); } } /** * Delete a folder plus every sub-folder and file beneath it. Returns the * deleted folder's path (so the caller can remove it from disk), or null. */ function delete_folder(int $id): ?string { $folder = get_folder($id); if (!$folder) { return null; } $repoId = (int) $folder['repo_id']; $path = $folder['path']; // Escape LIKE wildcards in the stored path (safe_relpath permits "_"). $prefix = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $path) . '/%'; // Files that live inside the folder. $stmt = db()->prepare('DELETE FROM files WHERE repo_id = ? AND filename LIKE ?'); $stmt->execute([$repoId, $prefix]); // The folder row itself and all descendant folder rows. $stmt = db()->prepare('DELETE FROM folders WHERE repo_id = ? AND (path = ? OR path LIKE ?)'); $stmt->execute([$repoId, $path, $prefix]); return $path; }