repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

pico / db
Eric Bower · 13 Dec 24

db.go

  1package db
  2
  3import (
  4	"database/sql"
  5	"database/sql/driver"
  6	"encoding/json"
  7	"errors"
  8	"regexp"
  9	"time"
 10)
 11
 12var ErrNameTaken = errors.New("username has already been claimed")
 13var ErrNameDenied = errors.New("username is on the denylist")
 14var ErrNameInvalid = errors.New("username has invalid characters in it")
 15var ErrPublicKeyTaken = errors.New("public key is already associated with another user")
 16
 17type PublicKey struct {
 18	ID        string     `json:"id"`
 19	UserID    string     `json:"user_id"`
 20	Name      string     `json:"name"`
 21	Key       string     `json:"key"`
 22	CreatedAt *time.Time `json:"created_at"`
 23}
 24
 25type User struct {
 26	ID        string     `json:"id"`
 27	Name      string     `json:"name"`
 28	PublicKey *PublicKey `json:"public_key,omitempty"`
 29	CreatedAt *time.Time `json:"created_at"`
 30}
 31
 32type PostData struct {
 33	ImgPath    string     `json:"img_path"`
 34	LastDigest *time.Time `json:"last_digest"`
 35	Attempts   int        `json:"attempts"`
 36}
 37
 38// Make the Attrs struct implement the driver.Valuer interface. This method
 39// simply returns the JSON-encoded representation of the struct.
 40func (p PostData) Value() (driver.Value, error) {
 41	return json.Marshal(p)
 42}
 43
 44// Make the Attrs struct implement the sql.Scanner interface. This method
 45// simply decodes a JSON-encoded value into the struct fields.
 46func (p *PostData) Scan(value interface{}) error {
 47	b, ok := value.([]byte)
 48	if !ok {
 49		return errors.New("type assertion to []byte failed")
 50	}
 51
 52	return json.Unmarshal(b, &p)
 53}
 54
 55type Project struct {
 56	ID         string     `json:"id"`
 57	UserID     string     `json:"user_id"`
 58	Name       string     `json:"name"`
 59	ProjectDir string     `json:"project_dir"`
 60	Username   string     `json:"username"`
 61	Acl        ProjectAcl `json:"acl"`
 62	Blocked    string     `json:"blocked"`
 63	CreatedAt  *time.Time `json:"created_at"`
 64	UpdatedAt  *time.Time `json:"updated_at"`
 65}
 66
 67type ProjectAcl struct {
 68	Type string   `json:"type"`
 69	Data []string `json:"data"`
 70}
 71
 72// Make the Attrs struct implement the driver.Valuer interface. This method
 73// simply returns the JSON-encoded representation of the struct.
 74func (p ProjectAcl) Value() (driver.Value, error) {
 75	return json.Marshal(p)
 76}
 77
 78// Make the Attrs struct implement the sql.Scanner interface. This method
 79// simply decodes a JSON-encoded value into the struct fields.
 80func (p *ProjectAcl) Scan(value interface{}) error {
 81	b, ok := value.([]byte)
 82	if !ok {
 83		return errors.New("type assertion to []byte failed")
 84	}
 85
 86	return json.Unmarshal(b, &p)
 87}
 88
 89type FeedItemData struct {
 90	Title       string     `json:"title"`
 91	Description string     `json:"description"`
 92	Content     string     `json:"content"`
 93	Link        string     `json:"link"`
 94	PublishedAt *time.Time `json:"published_at"`
 95}
 96
 97// Make the Attrs struct implement the driver.Valuer interface. This method
 98// simply returns the JSON-encoded representation of the struct.
 99func (p FeedItemData) Value() (driver.Value, error) {
100	return json.Marshal(p)
101}
102
103// Make the Attrs struct implement the sql.Scanner interface. This method
104// simply decodes a JSON-encoded value into the struct fields.
105func (p *FeedItemData) Scan(value interface{}) error {
106	b, ok := value.([]byte)
107	if !ok {
108		return errors.New("type assertion to []byte failed")
109	}
110	return json.Unmarshal(b, &p)
111}
112
113type Post struct {
114	ID          string     `json:"id"`
115	UserID      string     `json:"user_id"`
116	Filename    string     `json:"filename"`
117	Slug        string     `json:"slug"`
118	Title       string     `json:"title"`
119	Text        string     `json:"text"`
120	Description string     `json:"description"`
121	CreatedAt   *time.Time `json:"created_at"`
122	PublishAt   *time.Time `json:"publish_at"`
123	Username    string     `json:"username"`
124	UpdatedAt   *time.Time `json:"updated_at"`
125	ExpiresAt   *time.Time `json:"expires_at"`
126	Hidden      bool       `json:"hidden"`
127	Views       int        `json:"views"`
128	Space       string     `json:"space"`
129	Shasum      string     `json:"shasum"`
130	FileSize    int        `json:"file_size"`
131	MimeType    string     `json:"mime_type"`
132	Data        PostData   `json:"data"`
133	Tags        []string   `json:"tags"`
134}
135
136type Paginate[T any] struct {
137	Data  []T
138	Total int
139}
140
141type Analytics struct {
142	TotalUsers     int
143	UsersLastMonth int
144	TotalPosts     int
145	PostsLastMonth int
146	UsersWithPost  int
147}
148
149type VisitInterval struct {
150	Interval *time.Time `json:"interval"`
151	Visitors int        `json:"visitors"`
152}
153
154type VisitUrl struct {
155	Url   string `json:"url"`
156	Count int    `json:"count"`
157}
158
159type SummaryOpts struct {
160	Interval string
161	Origin   time.Time
162	Host     string
163	Path     string
164	UserID   string
165}
166
167type SummaryVisits struct {
168	Intervals    []*VisitInterval `json:"intervals"`
169	TopUrls      []*VisitUrl      `json:"top_urls"`
170	NotFoundUrls []*VisitUrl      `json:"not_found_urls"`
171	TopReferers  []*VisitUrl      `json:"top_referers"`
172}
173
174type PostAnalytics struct {
175	ID       string
176	PostID   string
177	Views    int
178	UpdateAt *time.Time
179}
180
181type AnalyticsVisits struct {
182	ID          string `json:"id"`
183	UserID      string `json:"user_id"`
184	ProjectID   string `json:"project_id"`
185	PostID      string `json:"post_id"`
186	Namespace   string `json:"namespace"`
187	Host        string `json:"host"`
188	Path        string `json:"path"`
189	IpAddress   string `json:"ip_address"`
190	UserAgent   string `json:"user_agent"`
191	Referer     string `json:"referer"`
192	Status      int    `json:"status"`
193	ContentType string `json:"content_type"`
194}
195
196type Pager struct {
197	Num  int
198	Page int
199}
200
201type FeedItem struct {
202	ID        string
203	PostID    string
204	GUID      string
205	Data      FeedItemData
206	CreatedAt *time.Time
207}
208
209type Token struct {
210	ID        string     `json:"id"`
211	UserID    string     `json:"user_id"`
212	Name      string     `json:"name"`
213	CreatedAt *time.Time `json:"created_at"`
214	ExpiresAt *time.Time `json:"expires_at"`
215}
216
217type FeatureFlag struct {
218	ID               string          `json:"id"`
219	UserID           string          `json:"user_id"`
220	PaymentHistoryID string          `json:"payment_history_id"`
221	Name             string          `json:"name"`
222	CreatedAt        *time.Time      `json:"created_at"`
223	ExpiresAt        *time.Time      `json:"expires_at"`
224	Data             FeatureFlagData `json:"data"`
225}
226
227func NewFeatureFlag(userID, name string, storageMax uint64, fileMax int64, specialFileMax int64) *FeatureFlag {
228	return &FeatureFlag{
229		UserID: userID,
230		Name:   name,
231		Data: FeatureFlagData{
232			StorageMax:     storageMax,
233			FileMax:        fileMax,
234			SpecialFileMax: specialFileMax,
235		},
236	}
237}
238
239func (ff *FeatureFlag) FindStorageMax(defaultSize uint64) uint64 {
240	if ff.Data.StorageMax == 0 {
241		return defaultSize
242	}
243	return ff.Data.StorageMax
244}
245
246func (ff *FeatureFlag) FindFileMax(defaultSize int64) int64 {
247	if ff.Data.FileMax == 0 {
248		return defaultSize
249	}
250	return ff.Data.FileMax
251}
252
253func (ff *FeatureFlag) FindSpecialFileMax(defaultSize int64) int64 {
254	if ff.Data.SpecialFileMax == 0 {
255		return defaultSize
256	}
257	return ff.Data.SpecialFileMax
258}
259
260func (ff *FeatureFlag) IsValid() bool {
261	if ff.ExpiresAt.IsZero() {
262		return false
263	}
264	return ff.ExpiresAt.After(time.Now())
265}
266
267type FeatureFlagData struct {
268	StorageMax     uint64 `json:"storage_max"`
269	FileMax        int64  `json:"file_max"`
270	SpecialFileMax int64  `json:"special_file_max"`
271}
272
273// Make the Attrs struct implement the driver.Valuer interface. This method
274// simply returns the JSON-encoded representation of the struct.
275func (p FeatureFlagData) Value() (driver.Value, error) {
276	return json.Marshal(p)
277}
278
279// Make the Attrs struct implement the sql.Scanner interface. This method
280// simply decodes a JSON-encoded value into the struct fields.
281func (p *FeatureFlagData) Scan(value interface{}) error {
282	b, ok := value.([]byte)
283	if !ok {
284		return errors.New("type assertion to []byte failed")
285	}
286	return json.Unmarshal(b, &p)
287}
288
289type PaymentHistoryData struct {
290	Notes string `json:"notes"`
291	TxID  string `json:"tx_id"`
292}
293
294// Make the Attrs struct implement the driver.Valuer interface. This method
295// simply returns the JSON-encoded representation of the struct.
296func (p PaymentHistoryData) Value() (driver.Value, error) {
297	return json.Marshal(p)
298}
299
300// Make the Attrs struct implement the sql.Scanner interface. This method
301// simply decodes a JSON-encoded value into the struct fields.
302func (p *PaymentHistoryData) Scan(value interface{}) error {
303	b, ok := value.([]byte)
304	if !ok {
305		return errors.New("type assertion to []byte failed")
306	}
307	return json.Unmarshal(b, &p)
308}
309
310type ErrMultiplePublicKeys struct{}
311
312func (m *ErrMultiplePublicKeys) Error() string {
313	return "there are multiple users with this public key, you must provide the username when using SSH: `ssh <user>@<domain>`\n"
314}
315
316var NameValidator = regexp.MustCompile("^[a-zA-Z0-9]{1,50}$")
317var DenyList = []string{
318	"admin",
319	"abuse",
320	"cgi",
321	"ops",
322	"help",
323	"spec",
324	"root",
325	"new",
326	"create",
327	"www",
328	"public",
329}
330
331type DB interface {
332	RegisterUser(name, pubkey, comment string) (*User, error)
333	RemoveUsers(userIDs []string) error
334	UpdatePublicKey(pubkeyID, name string) (*PublicKey, error)
335	InsertPublicKey(userID, pubkey, name string, tx *sql.Tx) error
336	FindPublicKeyForKey(pubkey string) (*PublicKey, error)
337	FindPublicKey(pubkeyID string) (*PublicKey, error)
338	FindKeysForUser(user *User) ([]*PublicKey, error)
339	RemoveKeys(pubkeyIDs []string) error
340
341	FindSiteAnalytics(space string) (*Analytics, error)
342
343	FindUsers() ([]*User, error)
344	FindUserForName(name string) (*User, error)
345	FindUserForNameAndKey(name string, pubkey string) (*User, error)
346	FindUserForKey(name string, pubkey string) (*User, error)
347	FindUser(userID string) (*User, error)
348	ValidateName(name string) (bool, error)
349	SetUserName(userID string, name string) error
350
351	FindUserForToken(token string) (*User, error)
352	FindTokensForUser(userID string) ([]*Token, error)
353	InsertToken(userID, name string) (string, error)
354	UpsertToken(userID, name string) (string, error)
355	FindTokenByName(userID, name string) (string, error)
356	RemoveToken(tokenID string) error
357
358	FindPosts() ([]*Post, error)
359	FindPost(postID string) (*Post, error)
360	FindPostsForUser(pager *Pager, userID string, space string) (*Paginate[*Post], error)
361	FindAllPostsForUser(userID string, space string) ([]*Post, error)
362	FindPostsBeforeDate(date *time.Time, space string) ([]*Post, error)
363	FindExpiredPosts(space string) ([]*Post, error)
364	FindUpdatedPostsForUser(userID string, space string) ([]*Post, error)
365	FindPostWithFilename(filename string, userID string, space string) (*Post, error)
366	FindPostWithSlug(slug string, userID string, space string) (*Post, error)
367	FindAllPosts(pager *Pager, space string) (*Paginate[*Post], error)
368	FindAllUpdatedPosts(pager *Pager, space string) (*Paginate[*Post], error)
369	InsertPost(post *Post) (*Post, error)
370	UpdatePost(post *Post) (*Post, error)
371	RemovePosts(postIDs []string) error
372
373	ReplaceTagsForPost(tags []string, postID string) error
374	FindUserPostsByTag(pager *Pager, tag, userID, space string) (*Paginate[*Post], error)
375	FindPostsByTag(pager *Pager, tag, space string) (*Paginate[*Post], error)
376	FindPopularTags(space string) ([]string, error)
377	FindTagsForPost(postID string) ([]string, error)
378
379	ReplaceAliasesForPost(aliases []string, postID string) error
380
381	InsertVisit(view *AnalyticsVisits) error
382	VisitSummary(opts *SummaryOpts) (*SummaryVisits, error)
383	FindVisitSiteList(opts *SummaryOpts) ([]*VisitUrl, error)
384
385	AddPicoPlusUser(username, email, paymentType, txId string) error
386	FindFeatureForUser(userID string, feature string) (*FeatureFlag, error)
387	FindFeaturesForUser(userID string) ([]*FeatureFlag, error)
388	HasFeatureForUser(userID string, feature string) bool
389	FindTotalSizeForUser(userID string) (int, error)
390	InsertFeature(userID, name string, expiresAt time.Time) (*FeatureFlag, error)
391	RemoveFeature(userID, names string) error
392
393	InsertFeedItems(postID string, items []*FeedItem) error
394	FindFeedItemsByPostID(postID string) ([]*FeedItem, error)
395
396	InsertProject(userID, name, projectDir string) (string, error)
397	UpdateProject(userID, name string) error
398	UpdateProjectAcl(userID, name string, acl ProjectAcl) error
399	LinkToProject(userID, projectID, projectDir string, commit bool) error
400	RemoveProject(projectID string) error
401	FindProjectByName(userID, name string) (*Project, error)
402	FindProjectLinks(userID, name string) ([]*Project, error)
403	FindProjectsByUser(userID string) ([]*Project, error)
404	FindProjectsByPrefix(userID, name string) ([]*Project, error)
405	FindAllProjects(page *Pager, by string) (*Paginate[*Project], error)
406
407	Close() error
408}