Tweaked project structure (#88)

This commit is contained in:
Thomas Miceli
2023-09-03 00:30:57 +02:00
committed by GitHub
parent 25316d7bf2
commit a7b346d8df
25 changed files with 122 additions and 116 deletions

View File

@ -0,0 +1,64 @@
package db
import (
"gorm.io/gorm/clause"
)
type AdminSetting struct {
Key string `gorm:"uniqueIndex"`
Value string
}
const (
SettingDisableSignup = "disable-signup"
SettingRequireLogin = "require-login"
SettingDisableLoginForm = "disable-login-form"
SettingDisableGravatar = "disable-gravatar"
)
func GetSetting(key string) (string, error) {
var setting AdminSetting
err := db.Where("key = ?", key).First(&setting).Error
return setting.Value, err
}
func GetSettings() (map[string]string, error) {
var settings []AdminSetting
err := db.Find(&settings).Error
if err != nil {
return nil, err
}
result := make(map[string]string)
for _, setting := range settings {
result[setting.Key] = setting.Value
}
return result, nil
}
func UpdateSetting(key string, value string) error {
return db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "key"}}, // key column
DoUpdates: clause.AssignmentColumns([]string{"value"}),
}).Create(&AdminSetting{
Key: key,
Value: value,
}).Error
}
func setSetting(key string, value string) error {
return db.Create(&AdminSetting{Key: key, Value: value}).Error
}
func initAdminSettings(settings map[string]string) error {
for key, value := range settings {
if err := setSetting(key, value); err != nil {
if !IsUniqueConstraintViolation(err) {
return err
}
}
}
return nil
}

66
internal/db/db.go Normal file
View File

@ -0,0 +1,66 @@
package db
import (
"errors"
"github.com/mattn/go-sqlite3"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/utils"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"strings"
)
var db *gorm.DB
func Setup(dbPath string) error {
var err error
journalMode := strings.ToUpper(config.C.SqliteJournalMode)
if !utils.SliceContains([]string{"DELETE", "TRUNCATE", "PERSIST", "MEMORY", "WAL", "OFF"}, journalMode) {
log.Warn().Msg("Invalid SQLite journal mode: " + journalMode)
}
if db, err = gorm.Open(sqlite.Open(dbPath+"?_fk=true&_journal_mode="+journalMode), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
}); err != nil {
return err
}
if err = db.SetupJoinTable(&Gist{}, "Likes", &Like{}); err != nil {
return err
}
if err = db.SetupJoinTable(&User{}, "Liked", &Like{}); err != nil {
return err
}
if err = db.AutoMigrate(&User{}, &Gist{}, &SSHKey{}, &AdminSetting{}); err != nil {
return err
}
ApplyMigrations(db)
// Default admin setting values
return initAdminSettings(map[string]string{
SettingDisableSignup: "0",
SettingRequireLogin: "0",
SettingDisableLoginForm: "0",
SettingDisableGravatar: "0",
})
}
func CountAll(table interface{}) (int64, error) {
var count int64
err := db.Model(table).Count(&count).Error
return count, err
}
func IsUniqueConstraintViolation(err error) bool {
var sqliteErr sqlite3.Error
if errors.As(err, &sqliteErr) && sqliteErr.ExtendedCode == sqlite3.ErrConstraintUnique {
return true
}
return false
}

403
internal/db/gist.go Normal file
View File

@ -0,0 +1,403 @@
package db
import (
"github.com/thomiceli/opengist/internal/git"
"gorm.io/gorm"
"os/exec"
"strings"
"time"
)
type Gist struct {
ID uint `gorm:"primaryKey"`
Uuid string
Title string
Preview string
PreviewFilename string
Description string
Private int // 0: public, 1: unlisted, 2: private
UserID uint
User User
NbFiles int
NbLikes int
NbForks int
CreatedAt int64
UpdatedAt int64
Likes []User `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
Forked *Gist `gorm:"foreignKey:ForkedID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL"`
ForkedID uint
}
type Like struct {
UserID uint `gorm:"primaryKey"`
GistID uint `gorm:"primaryKey"`
CreatedAt int64
}
func (gist *Gist) BeforeDelete(tx *gorm.DB) error {
// Decrement fork counter if the gist was forked
err := tx.Model(&Gist{}).
Omit("updated_at").
Where("id = ?", gist.ForkedID).
UpdateColumn("nb_forks", gorm.Expr("nb_forks - 1")).Error
return err
}
func GetGist(user string, gistUuid string) (*Gist, error) {
gist := new(Gist)
err := db.Preload("User").Preload("Forked.User").
Where("gists.uuid = ? AND users.username like ?", gistUuid, user).
Joins("join users on gists.user_id = users.id").
First(&gist).Error
return gist, err
}
func GetGistByID(gistId string) (*Gist, error) {
gist := new(Gist)
err := db.Preload("User").Preload("Forked.User").
Where("gists.id = ?", gistId).
First(&gist).Error
return gist, err
}
func GetAllGistsForCurrentUser(currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
var gists []*Gist
err := db.Preload("User").Preload("Forked.User").
Where("gists.private = 0 or gists.user_id = ?", currentUserId).
Limit(11).
Offset(offset * 10).
Order(sort + "_at " + order).
Find(&gists).Error
return gists, err
}
func GetAllGists(offset int) ([]*Gist, error) {
var gists []*Gist
err := db.Preload("User").
Limit(11).
Offset(offset * 10).
Order("id asc").
Find(&gists).Error
return gists, err
}
func GetAllGistsFromSearch(currentUserId uint, query string, offset int, sort string, order string) ([]*Gist, error) {
var gists []*Gist
err := db.Preload("User").Preload("Forked.User").
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("gists.title like ? or gists.description like ?", "%"+query+"%", "%"+query+"%").
Limit(11).
Offset(offset * 10).
Order("gists." + sort + "_at " + order).
Find(&gists).Error
return gists, err
}
func gistsFromUserStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User").
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("users.id = ?", fromUserId).
Joins("join users on gists.user_id = users.id")
}
func GetAllGistsFromUser(fromUserId uint, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
var gists []*Gist
err := gistsFromUserStatement(fromUserId, currentUserId).Limit(11).
Offset(offset * 10).
Order("gists." + sort + "_at " + order).
Find(&gists).Error
return gists, err
}
func CountAllGistsFromUser(fromUserId uint, currentUserId uint) (int64, error) {
var count int64
err := gistsFromUserStatement(fromUserId, currentUserId).Model(&Gist{}).Count(&count).Error
return count, err
}
func likedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User").
Where("((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("likes.user_id = ?", fromUserId).
Joins("join likes on gists.id = likes.gist_id").
Joins("join users on likes.user_id = users.id")
}
func GetAllGistsLikedByUser(fromUserId uint, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
var gists []*Gist
err := likedStatement(fromUserId, currentUserId).Limit(11).
Offset(offset * 10).
Order("gists." + sort + "_at " + order).
Find(&gists).Error
return gists, err
}
func CountAllGistsLikedByUser(fromUserId uint, currentUserId uint) (int64, error) {
var count int64
err := likedStatement(fromUserId, currentUserId).Model(&Gist{}).Count(&count).Error
return count, err
}
func forkedStatement(fromUserId uint, currentUserId uint) *gorm.DB {
return db.Preload("User").Preload("Forked.User").
Where("gists.forked_id is not null and ((gists.private = 0) or (gists.private > 0 and gists.user_id = ?))", currentUserId).
Where("gists.user_id = ?", fromUserId).
Joins("join users on gists.user_id = users.id")
}
func GetAllGistsForkedByUser(fromUserId uint, currentUserId uint, offset int, sort string, order string) ([]*Gist, error) {
var gists []*Gist
err := forkedStatement(fromUserId, currentUserId).Limit(11).
Offset(offset * 10).
Order("gists." + sort + "_at " + order).
Find(&gists).Error
return gists, err
}
func CountAllGistsForkedByUser(fromUserId uint, currentUserId uint) (int64, error) {
var count int64
err := forkedStatement(fromUserId, currentUserId).Model(&Gist{}).Count(&count).Error
return count, err
}
func GetAllGistsRows() ([]*Gist, error) {
var gists []*Gist
err := db.Table("gists").
Preload("User").
Find(&gists).Error
return gists, err
}
func (gist *Gist) Create() error {
// avoids foreign key constraint error because the default value in the struct is 0
return db.Omit("forked_id").Create(&gist).Error
}
func (gist *Gist) CreateForked() error {
return db.Create(&gist).Error
}
func (gist *Gist) Update() error {
return db.Omit("forked_id").Save(&gist).Error
}
func (gist *Gist) Delete() error {
return db.Delete(&gist).Error
}
func (gist *Gist) SetLastActiveNow() error {
return db.Model(&Gist{}).
Where("id = ?", gist.ID).
Update("updated_at", time.Now().Unix()).Error
}
func (gist *Gist) AppendUserLike(user *User) error {
err := db.Model(&gist).Omit("updated_at").Update("nb_likes", gist.NbLikes+1).Error
if err != nil {
return err
}
return db.Model(&gist).Omit("updated_at").Association("Likes").Append(user)
}
func (gist *Gist) RemoveUserLike(user *User) error {
err := db.Model(&gist).Omit("updated_at").Update("nb_likes", gist.NbLikes-1).Error
if err != nil {
return err
}
return db.Model(&gist).Omit("updated_at").Association("Likes").Delete(user)
}
func (gist *Gist) IncrementForkCount() error {
return db.Model(&gist).Omit("updated_at").Update("nb_forks", gist.NbForks+1).Error
}
func (gist *Gist) GetForkParent(user *User) (*Gist, error) {
fork := new(Gist)
err := db.Preload("User").
Where("forked_id = ? and user_id = ?", gist.ID, user.ID).
First(&fork).Error
return fork, err
}
func (gist *Gist) GetUsersLikes(offset int) ([]*User, error) {
var users []*User
err := db.Model(&gist).
Where("gist_id = ?", gist.ID).
Limit(31).
Offset(offset * 30).
Association("Likes").Find(&users)
return users, err
}
func (gist *Gist) GetForks(currentUserId uint, offset int) ([]*Gist, error) {
var gists []*Gist
err := db.Model(&gist).Preload("User").
Where("forked_id = ?", gist.ID).
Where("(gists.private = 0) or (gists.private > 0 and gists.user_id = ?)", currentUserId).
Limit(11).
Offset(offset * 10).
Order("updated_at desc").
Find(&gists).Error
return gists, err
}
func (gist *Gist) CanWrite(user *User) bool {
return !(user == nil) && (gist.UserID == user.ID)
}
func (gist *Gist) InitRepository() error {
return git.InitRepository(gist.User.Username, gist.Uuid)
}
func (gist *Gist) DeleteRepository() error {
return git.DeleteRepository(gist.User.Username, gist.Uuid)
}
func (gist *Gist) Files(revision string) ([]*git.File, error) {
var files []*git.File
filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, revision)
if err != nil {
// if the revision or the file do not exist
if exiterr, ok := err.(*exec.ExitError); ok && exiterr.ExitCode() == 128 {
return nil, nil
}
return nil, err
}
for _, fileStr := range filesStr {
file, err := gist.File(revision, fileStr, true)
if err != nil {
return nil, err
}
files = append(files, file)
}
return files, err
}
func (gist *Gist) File(revision string, filename string, truncate bool) (*git.File, error) {
content, truncated, err := git.GetFileContent(gist.User.Username, gist.Uuid, revision, filename, truncate)
// if the revision or the file do not exist
if exiterr, ok := err.(*exec.ExitError); ok && exiterr.ExitCode() == 128 {
return nil, nil
}
return &git.File{
Filename: filename,
Content: content,
Truncated: truncated,
}, err
}
func (gist *Gist) Log(skip int) ([]*git.Commit, error) {
return git.GetLog(gist.User.Username, gist.Uuid, skip)
}
func (gist *Gist) NbCommits() (string, error) {
return git.GetNumberOfCommitsOfRepository(gist.User.Username, gist.Uuid)
}
func (gist *Gist) AddAndCommitFiles(files *[]FileDTO) error {
if err := git.CloneTmp(gist.User.Username, gist.Uuid, gist.Uuid, gist.User.Email); err != nil {
return err
}
for _, file := range *files {
if err := git.SetFileContent(gist.Uuid, file.Filename, file.Content); err != nil {
return err
}
}
if err := git.AddAll(gist.Uuid); err != nil {
return err
}
if err := git.CommitRepository(gist.Uuid, gist.User.Username, gist.User.Email); err != nil {
return err
}
return git.Push(gist.Uuid)
}
func (gist *Gist) ForkClone(username string, uuid string) error {
return git.ForkClone(gist.User.Username, gist.Uuid, username, uuid)
}
func (gist *Gist) UpdateServerInfo() error {
return git.UpdateServerInfo(gist.User.Username, gist.Uuid)
}
func (gist *Gist) RPC(service string) ([]byte, error) {
return git.RPC(gist.User.Username, gist.Uuid, service)
}
func (gist *Gist) UpdatePreviewAndCount() error {
filesStr, err := git.GetFilesOfRepository(gist.User.Username, gist.Uuid, "HEAD")
if err != nil {
return err
}
gist.NbFiles = len(filesStr)
if len(filesStr) == 0 {
gist.Preview = ""
gist.PreviewFilename = ""
} else {
file, err := gist.File("HEAD", filesStr[0], true)
if err != nil {
return err
}
split := strings.Split(file.Content, "\n")
if len(split) > 10 {
gist.Preview = strings.Join(split[:10], "\n")
} else {
gist.Preview = file.Content
}
gist.Preview = file.Content
gist.PreviewFilename = file.Filename
}
return gist.Update()
}
// -- DTO -- //
type GistDTO struct {
Title string `validate:"max=50" form:"title"`
Description string `validate:"max=150" form:"description"`
Private int `validate:"number,min=0,max=2" form:"private"`
Files []FileDTO `validate:"min=1,dive"`
}
type FileDTO struct {
Filename string `validate:"excludes=\x2f,excludes=\x5c,max=50"`
Content string `validate:"required"`
}
func (dto *GistDTO) ToGist() *Gist {
return &Gist{
Title: dto.Title,
Description: dto.Description,
Private: dto.Private,
}
}
func (dto *GistDTO) ToExistingGist(gist *Gist) *Gist {
gist.Title = dto.Title
gist.Description = dto.Description
return gist
}

103
internal/db/migration.go Normal file
View File

@ -0,0 +1,103 @@
package db
import (
"fmt"
"github.com/rs/zerolog/log"
"gorm.io/gorm"
)
type MigrationVersion struct {
ID uint `gorm:"primaryKey"`
Version uint
}
func ApplyMigrations(db *gorm.DB) error {
// Create migration table if it doesn't exist
if err := db.AutoMigrate(&MigrationVersion{}); err != nil {
log.Fatal().Err(err).Msg("Error creating migration version table")
return err
}
// Get the current migration version
var currentVersion MigrationVersion
db.First(&currentVersion)
// Define migrations
migrations := []struct {
Version uint
Func func(*gorm.DB) error
}{
{1, v1_modifyConstraintToSSHKeys},
{2, v2_lowercaseEmails},
// Add more migrations here as needed
}
// Apply migrations
for _, m := range migrations {
if m.Version > currentVersion.Version {
tx := db.Begin()
if err := tx.Error; err != nil {
log.Fatal().Err(err).Msg("Error starting transaction")
return err
}
if err := m.Func(db); err != nil {
log.Fatal().Err(err).Msg(fmt.Sprintf("Error applying migration %d:", m.Version))
tx.Rollback()
return err
} else {
if err = tx.Commit().Error; err != nil {
log.Fatal().Err(err).Msg(fmt.Sprintf("Error committing migration %d:", m.Version))
return err
}
currentVersion.Version = m.Version
db.Save(&currentVersion)
log.Info().Msg(fmt.Sprintf("Migration %d applied successfully", m.Version))
}
}
}
return nil
}
// Modify the constraint on the ssh_keys table to use ON DELETE CASCADE
func v1_modifyConstraintToSSHKeys(db *gorm.DB) error {
createSQL := `
CREATE TABLE ssh_keys_temp (
id integer primary key,
title text,
content text,
sha text,
created_at integer,
last_used_at integer,
user_id integer
constraint fk_users_ssh_keys references users(id) on update cascade on delete cascade
);
`
if err := db.Exec(createSQL).Error; err != nil {
return err
}
// Copy data from the old table to the new table
copySQL := `INSERT INTO ssh_keys_temp SELECT * FROM ssh_keys;`
if err := db.Exec(copySQL).Error; err != nil {
return err
}
// Drop the old table
dropSQL := `DROP TABLE ssh_keys;`
if err := db.Exec(dropSQL).Error; err != nil {
return err
}
// Rename the new table to the original table name
renameSQL := `ALTER TABLE ssh_keys_temp RENAME TO ssh_keys;`
return db.Exec(renameSQL).Error
}
func v2_lowercaseEmails(db *gorm.DB) error {
// Copy the lowercase emails into the new column
copySQL := `UPDATE users SET email = lower(email);`
return db.Exec(copySQL).Error
}

86
internal/db/sshkey.go Normal file
View File

@ -0,0 +1,86 @@
package db
import (
"crypto/sha256"
"encoding/base64"
"golang.org/x/crypto/ssh"
"gorm.io/gorm"
"time"
)
type SSHKey struct {
ID uint `gorm:"primaryKey"`
Title string
Content string
SHA string
CreatedAt int64
LastUsedAt int64
UserID uint
User User `validate:"-" `
}
func (sshKey *SSHKey) BeforeCreate(tx *gorm.DB) error {
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(sshKey.Content))
if err != nil {
return err
}
sha := sha256.Sum256(pubKey.Marshal())
sshKey.SHA = base64.StdEncoding.EncodeToString(sha[:])
return nil
}
func GetSSHKeysByUserID(userId uint) ([]*SSHKey, error) {
var sshKeys []*SSHKey
err := db.
Where("user_id = ?", userId).
Order("created_at asc").
Find(&sshKeys).Error
return sshKeys, err
}
func GetSSHKeyByID(sshKeyId uint) (*SSHKey, error) {
sshKey := new(SSHKey)
err := db.
Where("id = ?", sshKeyId).
First(&sshKey).Error
return sshKey, err
}
func SSHKeyDoesExists(sshKeyContent string) (*SSHKey, error) {
sshKey := new(SSHKey)
err := db.
Where("content like ?", sshKeyContent+"%").
First(&sshKey).Error
return sshKey, err
}
func (sshKey *SSHKey) Create() error {
return db.Create(&sshKey).Error
}
func (sshKey *SSHKey) Delete() error {
return db.Delete(&sshKey).Error
}
func SSHKeyLastUsedNow(sshKeyContent string) error {
return db.Model(&SSHKey{}).
Where("content = ?", sshKeyContent).
Update("last_used_at", time.Now().Unix()).Error
}
// -- DTO -- //
type SSHKeyDTO struct {
Title string `form:"title" validate:"required,max=50"`
Content string `form:"content" validate:"required"`
}
func (dto *SSHKeyDTO) ToSSHKey() *SSHKey {
return &SSHKey{
Title: dto.Title,
Content: dto.Content,
}
}

189
internal/db/user.go Normal file
View File

@ -0,0 +1,189 @@
package db
import (
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey"`
Username string `gorm:"uniqueIndex"`
Password string
IsAdmin bool
CreatedAt int64
Email string
MD5Hash string // for gravatar, if no Email is specified, the value is random
AvatarURL string
GithubID string
GiteaID string
Gists []Gist `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
SSHKeys []SSHKey `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
Liked []Gist `gorm:"many2many:likes;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
}
func (user *User) BeforeDelete(tx *gorm.DB) error {
// Decrement likes counter for all gists liked by this user
// The likes will be automatically deleted by the foreign key constraint
err := tx.Model(&Gist{}).
Omit("updated_at").
Where("id IN (?)", tx.
Select("gist_id").
Table("likes").
Where("user_id = ?", user.ID),
).
UpdateColumn("nb_likes", gorm.Expr("nb_likes - 1")).
Error
if err != nil {
return err
}
// Decrement forks counter for all gists forked by this user
return tx.Model(&Gist{}).
Omit("updated_at").
Where("id IN (?)", tx.
Select("forked_id").
Table("gists").
Where("user_id = ?", user.ID),
).
UpdateColumn("nb_forks", gorm.Expr("nb_forks - 1")).
Error
}
func UserExists(username string) (bool, error) {
var count int64
err := db.Model(&User{}).Where("username like ?", username).Count(&count).Error
return count > 0, err
}
func GetAllUsers(offset int) ([]*User, error) {
var users []*User
err := db.
Limit(11).
Offset(offset * 10).
Order("id asc").
Find(&users).Error
return users, err
}
func GetUserByUsername(username string) (*User, error) {
user := new(User)
err := db.
Where("username like ?", username).
First(&user).Error
return user, err
}
func GetUserById(userId uint) (*User, error) {
user := new(User)
err := db.
Where("id = ?", userId).
First(&user).Error
return user, err
}
func GetUsersFromEmails(emailsSet map[string]struct{}) (map[string]*User, error) {
var users []*User
emails := make([]string, 0, len(emailsSet))
for email := range emailsSet {
emails = append(emails, email)
}
err := db.
Where("email IN ?", emails).
Find(&users).Error
if err != nil {
return nil, err
}
userMap := make(map[string]*User)
for _, user := range users {
userMap[user.Email] = user
}
return userMap, nil
}
func SSHKeyExistsForUser(sshKey string, userId uint) (*SSHKey, error) {
key := new(SSHKey)
err := db.
Where("content = ?", sshKey).
Where("user_id = ?", userId).
First(&key).Error
return key, err
}
func GetUserByProvider(id string, provider string) (*User, error) {
user := new(User)
var err error
switch provider {
case "github":
err = db.Where("github_id = ?", id).First(&user).Error
case "gitea":
err = db.Where("gitea_id = ?", id).First(&user).Error
}
return user, err
}
func (user *User) Create() error {
return db.Create(&user).Error
}
func (user *User) Update() error {
return db.Save(&user).Error
}
func (user *User) Delete() error {
return db.Delete(&user).Error
}
func (user *User) SetAdmin() error {
return db.Model(&user).Update("is_admin", true).Error
}
func (user *User) HasLiked(gist *Gist) (bool, error) {
association := db.Model(&gist).Where("user_id = ?", user.ID).Association("Likes")
if association.Error != nil {
return false, association.Error
}
if association.Count() == 0 {
return false, nil
}
return true, nil
}
func (user *User) DeleteProviderID(provider string) error {
switch provider {
case "github":
return db.Model(&user).
Update("github_id", nil).
Update("avatar_url", nil).
Error
case "gitea":
return db.Model(&user).
Update("gitea_id", nil).
Update("avatar_url", nil).
Error
}
return nil
}
// -- DTO -- //
type UserDTO struct {
Username string `form:"username" validate:"required,max=24,alphanum,notreserved"`
Password string `form:"password" validate:"required"`
}
func (dto *UserDTO) ToUser() *User {
return &User{
Username: dto.Username,
Password: dto.Password,
}
}