1package archesrv
2
3import (
4 "database/sql"
5 "fmt"
6 "path/filepath"
7 "time"
8
9 "arche/internal/repo"
10)
11
12type RepoRecord struct {
13 ID int64
14 Name string
15 Description string
16 Visibility string
17 CreatedAt time.Time
18}
19
20func (r *RepoRecord) IsPublic() bool { return r.Visibility == "public" }
21
22func repoPath(dataDir, name string) string {
23 return filepath.Join(dataDir, name)
24}
25
26func (d *DB) CreateRepo(name, description, visibility string) (*RepoRecord, error) {
27 if visibility != "public" && visibility != "private" {
28 visibility = "private"
29 }
30 now := time.Now()
31 res, err := d.db.Exec(
32 "INSERT INTO repos(name,description,visibility,created_at) VALUES(?,?,?,?)",
33 name, description, visibility, now.Unix(),
34 )
35 if err != nil {
36 return nil, fmt.Errorf("create repo: %w", err)
37 }
38 id, _ := res.LastInsertId()
39 return &RepoRecord{
40 ID: id,
41 Name: name,
42 Description: description,
43 Visibility: visibility,
44 CreatedAt: now,
45 }, nil
46}
47
48func (d *DB) GetRepo(name string) (*RepoRecord, error) {
49 r := &RepoRecord{}
50 var ts int64
51 err := d.db.QueryRow(
52 "SELECT id, name, description, visibility, created_at FROM repos WHERE name=?", name,
53 ).Scan(&r.ID, &r.Name, &r.Description, &r.Visibility, &ts)
54 if err == sql.ErrNoRows {
55 return nil, nil
56 }
57 if err != nil {
58 return nil, err
59 }
60 r.CreatedAt = time.Unix(ts, 0)
61 return r, nil
62}
63
64func (d *DB) ListRepos() ([]RepoRecord, error) {
65 rows, err := d.db.Query(
66 "SELECT id, name, description, visibility, created_at FROM repos ORDER BY name",
67 )
68 if err != nil {
69 return nil, err
70 }
71 defer rows.Close()
72 var out []RepoRecord
73 for rows.Next() {
74 var r RepoRecord
75 var ts int64
76 if err := rows.Scan(&r.ID, &r.Name, &r.Description, &r.Visibility, &ts); err != nil {
77 return nil, err
78 }
79 r.CreatedAt = time.Unix(ts, 0)
80 out = append(out, r)
81 }
82 return out, rows.Err()
83}
84
85func (d *DB) DeleteRepo(name string) error {
86 _, err := d.db.Exec("DELETE FROM repos WHERE name=?", name)
87 return err
88}
89
90func (d *DB) CanRead(rec *RepoRecord, u *User) bool {
91 if rec.IsPublic() {
92 return true
93 }
94 if u == nil {
95 return false
96 }
97 if u.IsAdmin {
98 return true
99 }
100 return d.hasRole(rec.ID, u.ID, "read", "write", "admin")
101}
102
103func (d *DB) CanWrite(rec *RepoRecord, u *User) bool {
104 if u == nil {
105 return false
106 }
107 if u.IsAdmin {
108 return true
109 }
110 return d.hasRole(rec.ID, u.ID, "write", "admin")
111}
112
113func (d *DB) hasRole(repoID, userID int64, roles ...string) bool {
114 for _, role := range roles {
115 var count int
116 d.db.QueryRow( //nolint:errcheck
117 "SELECT COUNT(*) FROM repo_permissions WHERE repo_id=? AND user_id=? AND role=?",
118 repoID, userID, role,
119 ).Scan(&count)
120 if count > 0 {
121 return true
122 }
123 }
124 return false
125}
126
127func (d *DB) SetPermission(repoID, userID int64, role string) error {
128 _, err := d.db.Exec(
129 `INSERT INTO repo_permissions(repo_id,user_id,role) VALUES(?,?,?)
130 ON CONFLICT(repo_id,user_id) DO UPDATE SET role=excluded.role`,
131 repoID, userID, role,
132 )
133 return err
134}
135
136func (d *DB) RemovePermission(repoID, userID int64) error {
137 _, err := d.db.Exec("DELETE FROM repo_permissions WHERE repo_id=? AND user_id=?", repoID, userID)
138 return err
139}
140
141type CollabEntry struct {
142 UserID int64
143 Username string
144 Role string
145}
146
147func (d *DB) ListCollaborators(repoID int64) ([]CollabEntry, error) {
148 rows, err := d.db.Query(
149 `SELECT u.id, u.username, rp.role
150 FROM repo_permissions rp
151 JOIN users u ON u.id = rp.user_id
152 WHERE rp.repo_id = ?
153 ORDER BY u.username`,
154 repoID,
155 )
156 if err != nil {
157 return nil, err
158 }
159 defer rows.Close()
160 var out []CollabEntry
161 for rows.Next() {
162 var e CollabEntry
163 if err := rows.Scan(&e.UserID, &e.Username, &e.Role); err != nil {
164 return nil, err
165 }
166 out = append(out, e)
167 }
168 return out, rows.Err()
169}
170
171func (d *DB) UpdateRepo(name, description, visibility string) error {
172 if visibility != "public" && visibility != "private" {
173 visibility = "private"
174 }
175 _, err := d.db.Exec(
176 "UPDATE repos SET description=?, visibility=? WHERE name=?",
177 description, visibility, name,
178 )
179 return err
180}
181
182func (d *DB) GetRepoHookConfig(repoID int64) (allowShellHooks bool, postReceiveScript string, err error) {
183 err = d.db.QueryRow(
184 "SELECT allow_shell_hooks, post_receive_script FROM repos WHERE id=?", repoID,
185 ).Scan(&allowShellHooks, &postReceiveScript)
186 return
187}
188
189func (d *DB) SetRepoAllowShellHooks(repoID int64, allow bool, scriptPath string) error {
190 _, err := d.db.Exec(
191 "UPDATE repos SET allow_shell_hooks=?, post_receive_script=? WHERE id=?",
192 allow, scriptPath, repoID,
193 )
194 return err
195}
196
197func (d *DB) hasWriteCollaborator(repoID int64) bool {
198 var count int
199 d.db.QueryRow( //nolint:errcheck
200 `SELECT COUNT(*) FROM repo_permissions WHERE repo_id=? AND (role='write' OR role='admin')`,
201 repoID,
202 ).Scan(&count)
203 return count > 0
204}
205
206func openRepo(dataDir, name string) (*repo.Repo, error) {
207 return repo.Open(repoPath(dataDir, name))
208}