rin
Публичный Open-source lightweight git hosting.
db.go · 259 строк · 6.9 KB
Raw
1 package db
2
3 import (
4 "context"
5 "database/sql"
6 "embed"
7 "errors"
8 "fmt"
9 "io/fs"
10 "os"
11 "path/filepath"
12 "sort"
13 "strconv"
14 "strings"
15 "time"
16
17 _ "modernc.org/sqlite"
18 )
19
20 //go:embed migrations/*.sql
21 var migrationsFS embed.FS
22
23 type DB struct {
24 *sql.DB
25 }
26
27 func Open(ctx context.Context, dbPath string, defaultRegEnabled bool) (*DB, error) {
28 if dbPath != ":memory:" && !strings.HasPrefix(dbPath, "file::memory:") {
29 if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil {
30 return nil, fmt.Errorf("create database directory: %w", err)
31 }
32 }
33
34 dsn := dbPath
35 if !strings.Contains(dsn, "?") {
36 dsn += "?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)&_pragma=synchronous(NORMAL)"
37 }
38
39 sqlDB, err := sql.Open("sqlite", dsn)
40 if err != nil {
41 return nil, fmt.Errorf("open sqlite %s: %w", dbPath, err)
42 }
43
44 sqlDB.SetMaxOpenConns(1)
45 sqlDB.SetMaxIdleConns(1)
46 sqlDB.SetConnMaxLifetime(0)
47
48 pingCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
49 defer cancel()
50 if err := sqlDB.PingContext(pingCtx); err != nil {
51 _ = sqlDB.Close()
52 return nil, fmt.Errorf("ping sqlite: %w", err)
53 }
54
55 pragmas := []string{
56 "PRAGMA journal_mode = WAL;",
57 "PRAGMA foreign_keys = ON;",
58 "PRAGMA busy_timeout = 5000;",
59 "PRAGMA synchronous = NORMAL;",
60 }
61 for _, p := range pragmas {
62 if _, err := sqlDB.ExecContext(pingCtx, p); err != nil {
63 _ = sqlDB.Close()
64 return nil, fmt.Errorf("exec pragma %q: %w", p, err)
65 }
66 }
67
68 d := &DB{DB: sqlDB}
69 if err := d.Migrate(pingCtx); err != nil {
70 _ = sqlDB.Close()
71 return nil, fmt.Errorf("run migrations: %w", err)
72 }
73
74 if _, err := d.GetSetting(pingCtx, "registration_enabled"); errors.Is(err, sql.ErrNoRows) {
75 if err := d.SetSetting(pingCtx, "registration_enabled", strconv.FormatBool(defaultRegEnabled)); err != nil {
76 _ = sqlDB.Close()
77 return nil, fmt.Errorf("seed registration_enabled setting: %w", err)
78 }
79 }
80
81 if _, err := d.GetSetting(pingCtx, "registration_mode"); errors.Is(err, sql.ErrNoRows) {
82 mode := RegistrationModeClosed
83 if d.IsRegistrationEnabled(pingCtx, defaultRegEnabled) {
84 mode = RegistrationModePublic
85 }
86 if err := d.SetSetting(pingCtx, "registration_mode", mode); err != nil {
87 _ = sqlDB.Close()
88 return nil, fmt.Errorf("seed registration_mode setting: %w", err)
89 }
90 }
91
92 return d, nil
93 }
94
95 func (d *DB) Migrate(ctx context.Context) error {
96 _, err := d.ExecContext(ctx, `
97 CREATE TABLE IF NOT EXISTS schema_migrations (
98 version TEXT PRIMARY KEY,
99 applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
100 );
101 `)
102 if err != nil {
103 return fmt.Errorf("create schema_migrations table: %w", err)
104 }
105
106 entries, err := fs.ReadDir(migrationsFS, "migrations")
107 if err != nil {
108 return fmt.Errorf("read embedded migrations: %w", err)
109 }
110
111 var files []string
112 for _, entry := range entries {
113 if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".sql") {
114 files = append(files, entry.Name())
115 }
116 }
117 sort.Strings(files)
118
119 for _, name := range files {
120 var exists int
121 err := d.QueryRowContext(ctx, `SELECT 1 FROM schema_migrations WHERE version = ?`, name).Scan(&exists)
122 if err == nil {
123 continue
124 }
125 if !errors.Is(err, sql.ErrNoRows) {
126 return fmt.Errorf("check migration %s: %w", name, err)
127 }
128
129 content, err := migrationsFS.ReadFile("migrations/" + name)
130 if err != nil {
131 return fmt.Errorf("read migration %s: %w", name, err)
132 }
133
134 tx, err := d.BeginTx(ctx, nil)
135 if err != nil {
136 return fmt.Errorf("begin migration tx %s: %w", name, err)
137 }
138
139 if _, err := tx.ExecContext(ctx, string(content)); err != nil {
140 _ = tx.Rollback()
141 return fmt.Errorf("execute migration %s: %w", name, err)
142 }
143 if _, err := tx.ExecContext(ctx, `INSERT INTO schema_migrations (version) VALUES (?)`, name); err != nil {
144 _ = tx.Rollback()
145 return fmt.Errorf("record migration %s: %w", name, err)
146 }
147 if err := tx.Commit(); err != nil {
148 return fmt.Errorf("commit migration %s: %w", name, err)
149 }
150 }
151
152 return nil
153 }
154
155 func (d *DB) GetSetting(ctx context.Context, key string) (string, error) {
156 cctx, cancel := context.WithTimeout(ctx, 3*time.Second)
157 defer cancel()
158 var val string
159 err := d.QueryRowContext(cctx, `SELECT value FROM settings WHERE key = ?`, key).Scan(&val)
160 if err != nil {
161 return "", fmt.Errorf("get setting %s: %w", key, err)
162 }
163 return val, nil
164 }
165
166 func (d *DB) SetSetting(ctx context.Context, key, value string) error {
167 cctx, cancel := context.WithTimeout(ctx, 3*time.Second)
168 defer cancel()
169 _, err := d.ExecContext(cctx, `
170 INSERT INTO settings (key, value) VALUES (?, ?)
171 ON CONFLICT(key) DO UPDATE SET value = excluded.value
172 `, key, value)
173 if err != nil {
174 return fmt.Errorf("set setting %s: %w", key, err)
175 }
176 return nil
177 }
178
179 func (d *DB) GetBoolSetting(ctx context.Context, key string, fallback bool) bool {
180 val, err := d.GetSetting(ctx, key)
181 if err != nil {
182 return fallback
183 }
184 b, err := strconv.ParseBool(val)
185 if err != nil {
186 return fallback
187 }
188 return b
189 }
190
191 func (d *DB) SetBoolSetting(ctx context.Context, key string, enabled bool) error {
192 return d.SetSetting(ctx, key, strconv.FormatBool(enabled))
193 }
194
195 const (
196 RegistrationModePublic = "public"
197 RegistrationModeConfirm = "confirm"
198 RegistrationModeClosed = "closed"
199 )
200
201 func ValidRegistrationMode(mode string) bool {
202 switch mode {
203 case RegistrationModePublic, RegistrationModeConfirm, RegistrationModeClosed:
204 return true
205 default:
206 return false
207 }
208 }
209
210 // GetRegistrationMode returns the current registration policy. Older databases
211 // only have the boolean registration_enabled setting, so fall back to it.
212 func (d *DB) GetRegistrationMode(ctx context.Context, fallback bool) string {
213 mode, err := d.GetSetting(ctx, "registration_mode")
214 if err == nil && ValidRegistrationMode(mode) {
215 return mode
216 }
217 if d.IsRegistrationEnabled(ctx, fallback) {
218 return RegistrationModePublic
219 }
220 return RegistrationModeClosed
221 }
222
223 func (d *DB) SetRegistrationMode(ctx context.Context, mode string) error {
224 if !ValidRegistrationMode(mode) {
225 return fmt.Errorf("invalid registration mode %q", mode)
226 }
227 if err := d.SetSetting(ctx, "registration_mode", mode); err != nil {
228 return err
229 }
230 return d.SetBoolSetting(ctx, "registration_enabled", mode != RegistrationModeClosed)
231 }
232
233 func (d *DB) IsRegistrationEnabled(ctx context.Context, fallback bool) bool {
234 return d.GetBoolSetting(ctx, "registration_enabled", fallback)
235 }
236
237 func (d *DB) SetRegistrationEnabled(ctx context.Context, enabled bool) error {
238 mode := RegistrationModeClosed
239 if enabled {
240 mode = RegistrationModePublic
241 }
242 return d.SetRegistrationMode(ctx, mode)
243 }
244
245 func (d *DB) IsExploreEnabled(ctx context.Context) bool {
246 return d.GetBoolSetting(ctx, "explore_enabled", true)
247 }
248
249 func (d *DB) IsLandingEnabled(ctx context.Context, fallback bool) bool {
250 return d.GetBoolSetting(ctx, "landing_enabled", fallback)
251 }
252
253 func (d *DB) IsAnonymousReadEnabled(ctx context.Context) bool {
254 return d.GetBoolSetting(ctx, "anonymous_read_enabled", true)
255 }
256
257 func (d *DB) IsIssuesEnabled(ctx context.Context) bool {
258 return d.GetBoolSetting(ctx, "issues_enabled", true)
259 }
260