213 lines · 6.5 KB
Raw Download
1
<?php
2
/**
3
 * Database connection (PDO singleton) and CRUD helpers.
4
 * All queries use prepared statements.
5
 */
6
7
require_once __DIR__ . '/../config.php';
8
9
function db(): PDO
10
{
11
    static $pdo = null;
12
    if ($pdo === null) {
13
        $dsn = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=' . DB_CHARSET;
14
        $pdo = new PDO($dsn, DB_USER, DB_PASS, [
15
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
16
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
17
            PDO::ATTR_EMULATE_PREPARES   => false,
18
        ]);
19
    }
20
    return $pdo;
21
}
22
23
// ---- Repositories -----------------------------------------------------------
24
25
/**
26
 * Sort options for the repository list, keyed by the value used in the URL.
27
 * Each value is a fixed ORDER BY clause (never interpolate user input directly).
28
 */
29
const REPO_SORTS = [
30
    'newest' => 'created_at DESC, id DESC',
31
    'oldest' => 'created_at ASC, id ASC',
32
    'name'   => 'name ASC, id ASC',
33
    'slug'   => 'slug ASC',
34
];
35
36
function get_repositories(string $sort = 'newest'): array
37
{
38
    $order = REPO_SORTS[$sort] ?? REPO_SORTS['newest'];
39
    $stmt = db()->query('SELECT * FROM repositories ORDER BY ' . $order);
40
    return $stmt->fetchAll();
41
}
42
43
function get_repository_by_slug(string $slug): ?array
44
{
45
    $stmt = db()->prepare('SELECT * FROM repositories WHERE slug = ? LIMIT 1');
46
    $stmt->execute([$slug]);
47
    $row = $stmt->fetch();
48
    return $row ?: null;
49
}
50
51
function get_repository(int $id): ?array
52
{
53
    $stmt = db()->prepare('SELECT * FROM repositories WHERE id = ? LIMIT 1');
54
    $stmt->execute([$id]);
55
    $row = $stmt->fetch();
56
    return $row ?: null;
57
}
58
59
function create_repository(string $slug, string $name, string $desc, string $lang): int
60
{
61
    $stmt = db()->prepare(
62
        'INSERT INTO repositories (slug, name, description, language) VALUES (?, ?, ?, ?)'
63
    );
64
    $stmt->execute([$slug, $name, $desc, $lang]);
65
    return (int) db()->lastInsertId();
66
}
67
68
function update_repository(int $id, string $name, string $desc, string $lang): void
69
{
70
    $stmt = db()->prepare(
71
        'UPDATE repositories SET name = ?, description = ?, language = ? WHERE id = ?'
72
    );
73
    $stmt->execute([$name, $desc, $lang, $id]);
74
}
75
76
/**
77
 * Rename a repository's slug. Updates the repositories row and rewrites the
78
 * stored slug prefix on every file's `filepath` (which is "slug/rel-path").
79
 * Runs in a transaction so both tables stay consistent. The caller is
80
 * responsible for renaming the on-disk uploads/<slug> directory.
81
 */
82
function rename_repository(int $id, string $oldSlug, string $newSlug): void
83
{
84
    $pdo = db();
85
    $pdo->beginTransaction();
86
    try {
87
        $stmt = $pdo->prepare('UPDATE repositories SET slug = ? WHERE id = ?');
88
        $stmt->execute([$newSlug, $id]);
89
90
        // filepath looks like "oldslug/dir/file.ext" — swap the leading segment.
91
        $stmt = $pdo->prepare(
92
            'UPDATE files SET filepath = CONCAT(?, SUBSTRING(filepath, ?)) WHERE repo_id = ?'
93
        );
94
        $stmt->execute([$newSlug, strlen($oldSlug) + 1, $id]);
95
96
        $pdo->commit();
97
    } catch (Throwable $e) {
98
        $pdo->rollBack();
99
        throw $e;
100
    }
101
}
102
103
function delete_repository(int $id): void
104
{
105
    // Files rows are removed automatically via ON DELETE CASCADE.
106
    $stmt = db()->prepare('DELETE FROM repositories WHERE id = ?');
107
    $stmt->execute([$id]);
108
}
109
110
// ---- Files ------------------------------------------------------------------
111
112
function get_files_by_repo(int $repoId): array
113
{
114
    $stmt = db()->prepare('SELECT * FROM files WHERE repo_id = ? ORDER BY filename ASC');
115
    $stmt->execute([$repoId]);
116
    return $stmt->fetchAll();
117
}
118
119
function get_file_by_name(int $repoId, string $filename): ?array
120
{
121
    $stmt = db()->prepare('SELECT * FROM files WHERE repo_id = ? AND filename = ? LIMIT 1');
122
    $stmt->execute([$repoId, $filename]);
123
    $row = $stmt->fetch();
124
    return $row ?: null;
125
}
126
127
function get_file(int $id): ?array
128
{
129
    $stmt = db()->prepare('SELECT * FROM files WHERE id = ? LIMIT 1');
130
    $stmt->execute([$id]);
131
    $row = $stmt->fetch();
132
    return $row ?: null;
133
}
134
135
function create_file(int $repoId, string $filename, string $filepath, int $size): int
136
{
137
    // Replace an existing same-named file record for this repo (re-upload).
138
    $stmt = db()->prepare(
139
        'INSERT INTO files (repo_id, filename, filepath, filesize)
140
         VALUES (?, ?, ?, ?)
141
         ON DUPLICATE KEY UPDATE filepath = VALUES(filepath), filesize = VALUES(filesize)'
142
    );
143
    $stmt->execute([$repoId, $filename, $filepath, $size]);
144
    return (int) db()->lastInsertId();
145
}
146
147
function delete_file(int $id): void
148
{
149
    $stmt = db()->prepare('DELETE FROM files WHERE id = ?');
150
    $stmt->execute([$id]);
151
}
152
153
// ---- Folders ----------------------------------------------------------------
154
155
function get_folders_by_repo(int $repoId): array
156
{
157
    $stmt = db()->prepare('SELECT * FROM folders WHERE repo_id = ? ORDER BY path ASC');
158
    $stmt->execute([$repoId]);
159
    return $stmt->fetchAll();
160
}
161
162
function get_folder(int $id): ?array
163
{
164
    $stmt = db()->prepare('SELECT * FROM folders WHERE id = ? LIMIT 1');
165
    $stmt->execute([$id]);
166
    $row = $stmt->fetch();
167
    return $row ?: null;
168
}
169
170
/**
171
 * Register a folder path and all of its ancestors. For "a/b/c" this inserts
172
 * "a", "a/b" and "a/b/c". Existing rows are ignored (INSERT IGNORE).
173
 */
174
function create_folder(int $repoId, string $path): void
175
{
176
    $stmt  = db()->prepare('INSERT IGNORE INTO folders (repo_id, path) VALUES (?, ?)');
177
    $accum = [];
178
    foreach (explode('/', trim($path, '/')) as $seg) {
179
        if ($seg === '') {
180
            continue;
181
        }
182
        $accum[] = $seg;
183
        $stmt->execute([$repoId, implode('/', $accum)]);
184
    }
185
}
186
187
/**
188
 * Delete a folder plus every sub-folder and file beneath it. Returns the
189
 * deleted folder's path (so the caller can remove it from disk), or null.
190
 */
191
function delete_folder(int $id): ?string
192
{
193
    $folder = get_folder($id);
194
    if (!$folder) {
195
        return null;
196
    }
197
    $repoId = (int) $folder['repo_id'];
198
    $path   = $folder['path'];
199
200
    // Escape LIKE wildcards in the stored path (safe_relpath permits "_").
201
    $prefix = str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $path) . '/%';
202
203
    // Files that live inside the folder.
204
    $stmt = db()->prepare('DELETE FROM files WHERE repo_id = ? AND filename LIKE ?');
205
    $stmt->execute([$repoId, $prefix]);
206
207
    // The folder row itself and all descendant folder rows.
208
    $stmt = db()->prepare('DELETE FROM folders WHERE repo_id = ? AND (path = ? OR path LIKE ?)');
209
    $stmt->execute([$repoId, $path, $prefix]);
210
211
    return $path;
212
}
213