From c8e1328164e9ffbd681c3c0e449f1e6b9856b896 Mon Sep 17 00:00:00 2001 From: Gabriel Arakaki Giovanini Date: Sun, 26 Feb 2023 19:54:48 +0100 Subject: feat: Inicial commit It contains rough template for the server and runners. It contains rough template for the server and runners. --- pkg/database/localfs/filesystem.go | 49 ++++++++ pkg/database/sql/media.go | 238 +++++++++++++++++++++++++++++++++++++ pkg/database/sql/migration.go | 17 +++ pkg/database/sql/settings.go | 69 +++++++++++ pkg/database/sql/user.go | 182 ++++++++++++++++++++++++++++ pkg/database/sql/user_test.go | 110 +++++++++++++++++ 6 files changed, 665 insertions(+) create mode 100644 pkg/database/localfs/filesystem.go create mode 100644 pkg/database/sql/media.go create mode 100644 pkg/database/sql/migration.go create mode 100644 pkg/database/sql/settings.go create mode 100644 pkg/database/sql/user.go create mode 100644 pkg/database/sql/user_test.go (limited to 'pkg/database') diff --git a/pkg/database/localfs/filesystem.go b/pkg/database/localfs/filesystem.go new file mode 100644 index 0000000..c7c6458 --- /dev/null +++ b/pkg/database/localfs/filesystem.go @@ -0,0 +1,49 @@ +package localfs + +import ( + "io/fs" + "os" + "path" + "strings" +) + +type FileSystemRepository struct { + root string +} + +func NewFileSystemRepository(root string) *FileSystemRepository { + return &FileSystemRepository{ + root: root, + } +} + +func (self *FileSystemRepository) getFilesFromPath(filepath string) ([]fs.FileInfo, error) { + dirs, err := os.ReadDir(filepath) + if err != nil { + return nil, err + } + + infos := make([]fs.FileInfo, 0, len(dirs)) + for _, dir := range dirs { + if strings.HasPrefix(dir.Name(), ".") { + continue + } + info, err := dir.Info() + if err != nil { + return nil, err + } + infos = append(infos, info) + } + + return infos, nil +} + +func (self *FileSystemRepository) List(filepath string) ([]fs.FileInfo, error) { + workingPath := path.Join(self.root, filepath) + return self.getFilesFromPath(workingPath) +} + +func (self *FileSystemRepository) Stat(filepath string) (fs.FileInfo, error) { + workingPath := path.Join(self.root, filepath) + return os.Stat(workingPath) +} diff --git a/pkg/database/sql/media.go b/pkg/database/sql/media.go new file mode 100644 index 0000000..835e262 --- /dev/null +++ b/pkg/database/sql/media.go @@ -0,0 +1,238 @@ +package sql + +import ( + "context" + "time" + + "gorm.io/gorm" + + "git.sr.ht/~gabrielgio/img/pkg/components/media" + "git.sr.ht/~gabrielgio/img/pkg/list" +) + +type ( + Media struct { + gorm.Model + Name string `gorm:"not null"` + Path string `gorm:"not null;unique"` + PathHash string `gorm:"not null;unique"` + MIMEType string `gorm:"not null"` + } + + MediaEXIF struct { + gorm.Model + MediaID uint + Media Media + Description *string + Camera *string + Maker *string + Lens *string + DateShot *time.Time + Exposure *float64 + Aperture *float64 + Iso *int64 + FocalLength *float64 + Flash *int64 + Orientation *int64 + ExposureProgram *int64 + GPSLatitude *float64 + GPSLongitude *float64 + } + + MediaRepository struct { + db *gorm.DB + } +) + +var _ media.Repository = &MediaRepository{} + +func (self *Media) ToModel() *media.Media { + return &media.Media{ + ID: self.ID, + Path: self.Path, + PathHash: self.PathHash, + Name: self.Name, + MIMEType: self.MIMEType, + } +} + +func (m *MediaEXIF) ToModel() *media.MediaEXIF { + return &media.MediaEXIF{ + Description: m.Description, + Camera: m.Camera, + Maker: m.Maker, + Lens: m.Lens, + DateShot: m.DateShot, + Exposure: m.Exposure, + Aperture: m.Aperture, + Iso: m.Iso, + FocalLength: m.FocalLength, + Flash: m.Flash, + Orientation: m.Orientation, + ExposureProgram: m.ExposureProgram, + GPSLatitude: m.GPSLatitude, + GPSLongitude: m.GPSLongitude, + } +} + +func NewMediaRepository(db *gorm.DB) *MediaRepository { + return &MediaRepository{ + db: db, + } +} + +func (self *MediaRepository) Create(ctx context.Context, createMedia *media.CreateMedia) error { + media := &Media{ + Name: createMedia.Name, + Path: createMedia.Path, + PathHash: createMedia.PathHash, + MIMEType: createMedia.MIMEType, + } + + result := self.db. + WithContext(ctx). + Create(media) + if result.Error != nil { + return result.Error + } + + return nil +} + +func (self *MediaRepository) Exists(ctx context.Context, path string) (bool, error) { + var exists bool + result := self.db. + WithContext(ctx). + Model(&Media{}). + Select("count(id) > 0"). + Where("path_hash = ?", path). + Find(&exists) + + if result.Error != nil { + return false, result.Error + } + + return exists, nil +} + +func (self *MediaRepository) List(ctx context.Context, pagination *media.Pagination) ([]*media.Media, error) { + medias := make([]*Media, 0) + result := self.db. + WithContext(ctx). + Model(&Media{}). + Offset(pagination.Page * pagination.Size). + Limit(pagination.Size). + Order("created_at DESC"). + Find(&medias) + + if result.Error != nil { + return nil, result.Error + } + + m := list.Map(medias, func(s *Media) *media.Media { + return s.ToModel() + }) + + return m, nil +} + +func (self *MediaRepository) Get(ctx context.Context, pathHash string) (*media.Media, error) { + m := &Media{} + result := self.db. + WithContext(ctx). + Model(&Media{}). + Where("path_hash = ?", pathHash). + Limit(1). + Take(m) + + if result.Error != nil { + return nil, result.Error + } + + return m.ToModel(), nil +} + +func (self *MediaRepository) GetPath(ctx context.Context, pathHash string) (string, error) { + var path string + result := self.db. + WithContext(ctx). + Model(&Media{}). + Select("path"). + Where("path_hash = ?", pathHash). + Limit(1). + Find(&path) + + if result.Error != nil { + return "", result.Error + } + + return path, nil +} + +func (m *MediaRepository) GetEXIF(ctx context.Context, mediaID uint) (*media.MediaEXIF, error) { + exif := &MediaEXIF{} + result := m.db. + WithContext(ctx). + Model(&Media{}). + Where("media_id = ?", mediaID). + Limit(1). + Take(m) + + if result.Error != nil { + return nil, result.Error + } + + return exif.ToModel(), nil +} + +func (s *MediaRepository) CreateEXIF(ctx context.Context, id uint, info *media.MediaEXIF) error { + media := &MediaEXIF{ + MediaID: id, + Description: info.Description, + Camera: info.Camera, + Maker: info.Maker, + Lens: info.Lens, + DateShot: info.DateShot, + Exposure: info.Exposure, + Aperture: info.Aperture, + Iso: info.Iso, + FocalLength: info.FocalLength, + Flash: info.Flash, + Orientation: info.Orientation, + ExposureProgram: info.ExposureProgram, + GPSLatitude: info.GPSLatitude, + GPSLongitude: info.GPSLongitude, + } + + result := s.db. + WithContext(ctx). + Create(media) + if result.Error != nil { + return result.Error + } + + return nil +} + +func (r *MediaRepository) GetEmptyEXIF(ctx context.Context, pagination *media.Pagination) ([]*media.Media, error) { + medias := make([]*Media, 0) + result := r.db. + WithContext(ctx). + Model(&Media{}). + Joins("left join media_exifs on media.id = media_exifs.media_id"). + Where("media_exifs.media_id IS NULL"). + Offset(pagination.Page * pagination.Size). + Limit(pagination.Size). + Order("created_at DESC"). + Find(&medias) + + if result.Error != nil { + return nil, result.Error + } + + m := list.Map(medias, func(s *Media) *media.Media { + return s.ToModel() + }) + + return m, nil +} diff --git a/pkg/database/sql/migration.go b/pkg/database/sql/migration.go new file mode 100644 index 0000000..019eb91 --- /dev/null +++ b/pkg/database/sql/migration.go @@ -0,0 +1,17 @@ +package sql + +import "gorm.io/gorm" + +func Migrate(db *gorm.DB) error { + for _, m := range []any{ + &User{}, + &Settings{}, + &Media{}, + &MediaEXIF{}, + } { + if err := db.AutoMigrate(m); err != nil { + return err + } + } + return nil +} diff --git a/pkg/database/sql/settings.go b/pkg/database/sql/settings.go new file mode 100644 index 0000000..7ad718b --- /dev/null +++ b/pkg/database/sql/settings.go @@ -0,0 +1,69 @@ +package sql + +import ( + "context" + + "gorm.io/gorm" + + "git.sr.ht/~gabrielgio/img/pkg/components/settings" +) + +type ( + Settings struct { + gorm.Model + ShowMode bool + ShowOwner bool + } + + SettingsRepository struct { + db *gorm.DB + } +) + +var _ settings.Repository = &SettingsRepository{} + +func NewSettingsRespository(db *gorm.DB) *SettingsRepository { + return &SettingsRepository{ + db: db, + } +} + +func (self *SettingsRepository) ensureSettings(ctx context.Context) (*Settings, error) { + var ( + db = self.db.WithContext(ctx) + s = &Settings{} + ) + result := db.Limit(1).Find(s) + if result.Error != nil { + return nil, result.Error + } + + return s, nil +} + +func (self *SettingsRepository) Save(ctx context.Context, toSaveSettings *settings.Settings) error { + db := self.db.WithContext(ctx) + + s, err := self.ensureSettings(ctx) + if err != nil { + return err + } + + s.ShowMode = toSaveSettings.ShowMode + s.ShowOwner = toSaveSettings.ShowOwner + + result := db.Save(s) + return result.Error +} + +func (self *SettingsRepository) Load(ctx context.Context) (*settings.Settings, error) { + s, err := self.ensureSettings(ctx) + if err != nil { + return nil, err + } + + return &settings.Settings{ + ShowMode: s.ShowMode, + ShowOwner: s.ShowOwner, + }, nil +} diff --git a/pkg/database/sql/user.go b/pkg/database/sql/user.go new file mode 100644 index 0000000..d449b05 --- /dev/null +++ b/pkg/database/sql/user.go @@ -0,0 +1,182 @@ +package sql + +import ( + "context" + + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" + + "git.sr.ht/~gabrielgio/img/pkg/components/auth" + user "git.sr.ht/~gabrielgio/img/pkg/components/auth" +) + +type ( + User struct { + gorm.Model + Username string + Name string + Password string + } + + Users []*User + + UserRepository struct { + db *gorm.DB + } +) + +var _ auth.Repository = &UserRepository{} + +func NewUserRepository(db *gorm.DB) *UserRepository { + return &UserRepository{ + db: db, + } +} + +func (self *User) ToModel() *user.User { + return &user.User{ + ID: self.Model.ID, + Name: self.Name, + Username: self.Username, + } +} + +func (self Users) ToModel() (users []*user.User) { + for _, user := range self { + users = append(users, user.ToModel()) + } + return +} + +// Testing function, will remove later +// TODO: remove later +func (self *UserRepository) EnsureAdmin(ctx context.Context) { + var exists bool + self.db. + WithContext(ctx). + Model(&User{}). + Select("count(*) > 0"). + Where("username = ?", "admin"). + Find(&exists) + + if !exists { + hash, _ := bcrypt.GenerateFromPassword([]byte("admin"), bcrypt.MinCost) + self.db.Save(&User{ + Username: "admin", + Password: string(hash), + }) + } +} + +func (self *UserRepository) List(ctx context.Context) ([]*user.User, error) { + users := Users{} + result := self.db. + WithContext(ctx). + Find(&users) + + if result.Error != nil { + return nil, result.Error + } + + return users.ToModel(), nil +} + +func (self *UserRepository) Get(ctx context.Context, id uint) (*user.User, error) { + var user = &user.User{ID: id} + result := self.db. + WithContext(ctx). + First(user) + + if result.Error != nil { + return nil, result.Error + } + + return user, nil +} + +func (self *UserRepository) GetIDByUsername(ctx context.Context, username string) (uint, error) { + userID := struct { + ID uint + }{} + + result := self.db. + WithContext(ctx). + Model(&User{}). + Where("username = ?", username). + First(&userID) + + if result.Error != nil { + return 0, result.Error + } + + return userID.ID, nil +} + +func (self *UserRepository) GetPassword(ctx context.Context, id uint) ([]byte, error) { + userPassword := struct { + Password []byte + }{} + + result := self.db. + WithContext(ctx). + Model(&User{}). + Where("id = ?", id). + First(&userPassword) + + if result.Error != nil { + return nil, result.Error + } + + return userPassword.Password, nil +} + +func (self *UserRepository) Create(ctx context.Context, createUser *user.CreateUser) (uint, error) { + user := &User{ + Username: createUser.Username, + Name: createUser.Name, + Password: string(createUser.Password), + } + + result := self.db. + WithContext(ctx). + Create(user) + if result.Error != nil { + return 0, result.Error + } + + return user.Model.ID, nil +} + +func (self *UserRepository) Update(ctx context.Context, id uint, update *user.UpdateUser) error { + user := &User{ + Model: gorm.Model{ + ID: id, + }, + Username: update.Username, + Name: update.Name, + } + + result := self.db. + WithContext(ctx). + Save(user) + if result.Error != nil { + return result.Error + } + + return nil +} + +func (self *UserRepository) Delete(ctx context.Context, id uint) error { + userID := struct { + ID uint + }{ + ID: id, + } + result := self.db. + WithContext(ctx). + Delete(userID) + if result.Error != nil { + return result.Error + } + return nil +} diff --git a/pkg/database/sql/user_test.go b/pkg/database/sql/user_test.go new file mode 100644 index 0000000..875b8e6 --- /dev/null +++ b/pkg/database/sql/user_test.go @@ -0,0 +1,110 @@ +//go:build integration + +package sql + +import ( + "context" + "os" + "testing" + + "github.com/google/go-cmp/cmp" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" + + "git.sr.ht/~gabrielgio/img/pkg/components/auth" +) + +func setup(t *testing.T) (*gorm.DB, func()) { + t.Helper() + + file, err := os.CreateTemp("", "img_user_*.db") + if err != nil { + t.Fatalf("Error creating tmp error: %s", err.Error()) + } + + db, err := gorm.Open(sqlite.Open(file.Name()), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), + }) + if err != nil { + t.Fatalf("Error openning db, error %s", err.Error()) + } + + err = Migrate(db) + if err != nil { + t.Fatalf("Error migrating db, error %s", err.Error()) + } + + return db, func() { + //nolint:errcheck + os.Remove(file.Name()) + } +} + +func TestCreate(t *testing.T) { + t.Parallel() + db, tearDown := setup(t) + defer tearDown() + + repository := NewUserRepository(db) + + id, err := repository.Create(context.Background(), &auth.CreateUser{ + Username: "new_username", + Name: "new_name", + }) + if err != nil { + t.Fatalf("Error creating: %s", err.Error()) + } + + got, err := repository.Get(context.Background(), id) + if err != nil { + t.Fatalf("Error getting: %s", err.Error()) + } + want := &auth.User{ + ID: id, + Username: "new_username", + Name: "new_name", + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("%s() mismatch (-want +got):\n%s", "Update", diff) + } +} + +func TestUpdate(t *testing.T) { + t.Parallel() + db, tearDown := setup(t) + defer tearDown() + + repository := NewUserRepository(db) + + id, err := repository.Create(context.Background(), &auth.CreateUser{ + Username: "username", + Name: "name", + }) + if err != nil { + t.Fatalf("Error creating user: %s", err.Error()) + } + + err = repository.Update(context.Background(), id, &auth.UpdateUser{ + Username: "new_username", + Name: "new_name", + }) + if err != nil { + t.Fatalf("Error update user: %s", err.Error()) + } + + got, err := repository.Get(context.Background(), id) + if err != nil { + t.Fatalf("Error getting user: %s", err.Error()) + } + want := &auth.User{ + ID: id, + Username: "new_username", + Name: "new_name", + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("%s() mismatch (-want +got):\n%s", "Update", diff) + } +} -- cgit v1.2.3