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