a commit that changes nothing?

This commit is contained in:
wisplite
2025-10-29 00:24:50 -05:00
parent c77f7c175a
commit ccf644acef
13 changed files with 317 additions and 317 deletions
+20 -20
View File
@@ -1,21 +1,21 @@
{ {
"go.useLanguageServer": true, "go.useLanguageServer": true,
"go.formatTool": "goimports", "go.formatTool": "goimports",
"go.lintOnSave": "package", "go.lintOnSave": "package",
"go.vetOnSave": "package", "go.vetOnSave": "package",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.organizeImports": "never", "source.organizeImports": "never",
"source.fixAll": "never" "source.fixAll": "never"
}, },
"[go]": { "[go]": {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.organizeImports": "never", "source.organizeImports": "never",
"source.fixAll": "never" "source.fixAll": "never"
} }
}, },
"go.buildOnSave": "off", "go.buildOnSave": "off",
"go.lintTool": "golint", "go.lintTool": "golint",
"go.toolsManagement.autoUpdate": true "go.toolsManagement.autoUpdate": true
} }
+45 -45
View File
@@ -1,46 +1,46 @@
## Raster: A Simple and Powerful Self-Hosted Photo/Video Management Solution ## Raster: A Simple and Powerful Self-Hosted Photo/Video Management Solution
Raster is a free and open-source photo/video management solution, with a focus on simplicity where it matters. Raster is a free and open-source photo/video management solution, with a focus on simplicity where it matters.
## Why Raster? ## Why Raster?
I started developing Raster because I found the other self-hosted galleries either lacking in features or difficult to install and maintain. I started developing Raster because I found the other self-hosted galleries either lacking in features or difficult to install and maintain.
I wanted a solution that was: I wanted a solution that was:
- Lightweight and easy to run - Lightweight and easy to run
- Simple, clean UI - Simple, clean UI
- Albums, content tags, and descriptions - Albums, content tags, and descriptions
- Public albums for showcasing work - Public albums for showcasing work
- Private shared albums with multi-user uploads - Private shared albums with multi-user uploads
- Comments and likes in shared albums - Comments and likes in shared albums
- Proper panorama support - Proper panorama support
- Modern tech stack - Modern tech stack
## Quick start ## Quick start
As of this moment, Raster is VERY early in development, and as such production builds are not ready yet. The instructions below are for testing and development. Do not use Raster for production until more progress has been made. As of this moment, Raster is VERY early in development, and as such production builds are not ready yet. The instructions below are for testing and development. Do not use Raster for production until more progress has been made.
Backend (API): Backend (API):
```bash ```bash
cd backend cd backend
go run ./cmd/server go run ./cmd/server
# Server runs on http://localhost:8080 # Server runs on http://localhost:8080
# A SQLite database (raster.db) will be created automatically in the backend directory. # A SQLite database (raster.db) will be created automatically in the backend directory.
``` ```
Frontend (web app): Frontend (web app):
```bash ```bash
cd frontend cd frontend
npm i npm i
npm run dev npm run dev
# App runs on http://localhost:5173 # App runs on http://localhost:5173
``` ```
## Project structure ## Project structure
- `backend/`: Go API using Gin + GORM (SQLite default) - `backend/`: Go API using Gin + GORM (SQLite default)
- `frontend/`: React app (Vite, Tailwind) - `frontend/`: React app (Vite, Tailwind)
## Status ## Status
In early development. Expect rapid updates. In early development. Expect rapid updates.
## Contributing ## Contributing
Issues and PRs are welcome. Issues and PRs are welcome.
+21 -21
View File
@@ -1,21 +1,21 @@
package main package main
import ( import (
"log" "log"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/wisplite/raster/internal/db" "github.com/wisplite/raster/internal/db"
"github.com/wisplite/raster/internal/routes" "github.com/wisplite/raster/internal/routes"
) )
func main() { func main() {
if !db.Init() { if !db.Init() {
log.Fatal("failed to initialize database") log.Fatal("failed to initialize database")
} }
r := gin.Default() r := gin.Default()
routes.RegisterRoutes(r) routes.RegisterRoutes(r)
r.Run(":8080") r.Run(":8080")
} }
+38 -38
View File
@@ -1,38 +1,38 @@
package db package db
import ( import (
"log" "log"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
"github.com/wisplite/raster/internal/models" "github.com/wisplite/raster/internal/models"
"gorm.io/gorm" "gorm.io/gorm"
) )
var db *gorm.DB var db *gorm.DB
func Init() bool { func Init() bool {
database, err := gorm.Open(sqlite.Open("raster.db"), &gorm.Config{}) database, err := gorm.Open(sqlite.Open("raster.db"), &gorm.Config{})
if err != nil { if err != nil {
log.Fatal("failed to connect database: ", err) log.Fatal("failed to connect database: ", err)
return false return false
} }
// Run migrations // Run migrations
err = database.AutoMigrate( err = database.AutoMigrate(
&models.Album{}, &models.Album{},
&models.User{}, &models.User{},
&models.AccessToken{}, &models.AccessToken{},
) )
if err != nil { if err != nil {
log.Fatal("failed to migrate database: ", err) log.Fatal("failed to migrate database: ", err)
return false return false
} }
db = database db = database
return true return true
} }
func GetDB() *gorm.DB { func GetDB() *gorm.DB {
return db return db
} }
+9 -9
View File
@@ -1,9 +1,9 @@
package models package models
import "time" import "time"
type AccessToken struct { type AccessToken struct {
Token string `gorm:"primaryKey"` Token string `gorm:"primaryKey"`
UserID string `gorm:"not null"` UserID string `gorm:"not null"`
Expires time.Time `gorm:"not null"` Expires time.Time `gorm:"not null"`
} }
+17 -17
View File
@@ -1,17 +1,17 @@
package models package models
import ( import (
"time" "time"
"gorm.io/datatypes" "gorm.io/datatypes"
) )
type Album struct { type Album struct {
ID string `gorm:"primaryKey"` ID string `gorm:"primaryKey"`
Title string `gorm:"not null"` Title string `gorm:"not null"`
Description string `gorm:"not null"` Description string `gorm:"not null"`
Tags datatypes.JSON `gorm:"type:json"` Tags datatypes.JSON `gorm:"type:json"`
Private bool `gorm:"not null"` Private bool `gorm:"not null"`
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
} }
+13 -13
View File
@@ -1,13 +1,13 @@
package models package models
import "time" import "time"
type User struct { type User struct {
ID string `gorm:"primaryKey"` ID string `gorm:"primaryKey"`
Username string `gorm:"not null"` Username string `gorm:"not null"`
Password string `gorm:"not null"` Password string `gorm:"not null"`
IsAdmin bool `gorm:"not null"` IsAdmin bool `gorm:"not null"`
IsActive bool `gorm:"not null"` IsActive bool `gorm:"not null"`
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
} }
+20 -20
View File
@@ -1,20 +1,20 @@
package routes package routes
import ( import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/wisplite/raster/internal/services" "github.com/wisplite/raster/internal/services"
) )
func RegisterAlbumRoutes(rg *gin.RouterGroup) { func RegisterAlbumRoutes(rg *gin.RouterGroup) {
album := rg.Group("/albums") album := rg.Group("/albums")
album.GET("/getPublic", func(c *gin.Context) { album.GET("/getPublic", func(c *gin.Context) {
albums, err := services.GetPublicAlbums() albums, err := services.GetPublicAlbums()
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
} }
c.JSON(http.StatusOK, albums) c.JSON(http.StatusOK, albums)
}) })
} }
+8 -8
View File
@@ -1,8 +1,8 @@
package routes package routes
import "github.com/gin-gonic/gin" import "github.com/gin-gonic/gin"
func RegisterRoutes(r *gin.Engine) { func RegisterRoutes(r *gin.Engine) {
rg := r.Group("/api") rg := r.Group("/api")
RegisterAlbumRoutes(rg) RegisterAlbumRoutes(rg)
} }
+32 -32
View File
@@ -1,32 +1,32 @@
package routes package routes
import ( import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/wisplite/raster/internal/services" "github.com/wisplite/raster/internal/services"
) )
func RegisterUserRoutes(rg *gin.RouterGroup) { func RegisterUserRoutes(rg *gin.RouterGroup) {
user := rg.Group("/user") user := rg.Group("/user")
user.POST("/createUser", func(c *gin.Context) { user.POST("/createUser", func(c *gin.Context) {
username := c.PostForm("username") username := c.PostForm("username")
password := c.PostForm("password") password := c.PostForm("password")
err := services.CreateUser(username, password) err := services.CreateUser(username, password)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
} }
c.JSON(http.StatusOK, gin.H{"message": "User created successfully"}) c.JSON(http.StatusOK, gin.H{"message": "User created successfully"})
}) })
user.POST("/login", func(c *gin.Context) { user.POST("/login", func(c *gin.Context) {
username := c.PostForm("username") username := c.PostForm("username")
password := c.PostForm("password") password := c.PostForm("password")
accessToken, err := services.Login(username, password) accessToken, err := services.Login(username, password)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
} }
c.JSON(http.StatusOK, gin.H{"message": "Login successful", "accessToken": accessToken.Token}) c.JSON(http.StatusOK, gin.H{"message": "Login successful", "accessToken": accessToken.Token})
}) })
} }
+24 -24
View File
@@ -1,24 +1,24 @@
package services package services
import ( import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/wisplite/raster/internal/db" "github.com/wisplite/raster/internal/db"
"github.com/wisplite/raster/internal/models" "github.com/wisplite/raster/internal/models"
) )
func CreateAccessToken(userID string) (models.AccessToken, error) { func CreateAccessToken(userID string) (models.AccessToken, error) {
token := uuid.New().String() token := uuid.New().String()
expires := time.Now().Add(time.Hour * 24 * 30) expires := time.Now().Add(time.Hour * 24 * 30)
accessToken := models.AccessToken{ accessToken := models.AccessToken{
Token: token, Token: token,
UserID: userID, UserID: userID,
Expires: expires, Expires: expires,
} }
result := db.GetDB().Create(&accessToken) result := db.GetDB().Create(&accessToken)
if result.Error != nil { if result.Error != nil {
return models.AccessToken{}, result.Error return models.AccessToken{}, result.Error
} }
return accessToken, nil return accessToken, nil
} }
+25 -25
View File
@@ -1,25 +1,25 @@
package services package services
import ( import (
"github.com/wisplite/raster/internal/db" "github.com/wisplite/raster/internal/db"
"github.com/wisplite/raster/internal/models" "github.com/wisplite/raster/internal/models"
) )
func GetPublicAlbums() ([]models.Album, error) { func GetPublicAlbums() ([]models.Album, error) {
albums := []models.Album{} albums := []models.Album{}
result := db.GetDB().Where("private = ?", false).Find(&albums) result := db.GetDB().Where("private = ?", false).Find(&albums)
if result.Error != nil { if result.Error != nil {
return []models.Album{}, result.Error return []models.Album{}, result.Error
} }
return albums, nil return albums, nil
} }
func GetAlbum(id string, authToken string) (models.Album, error) { func GetAlbum(id string, authToken string) (models.Album, error) {
// TODO: Add authentication // TODO: Add authentication
album := models.Album{} album := models.Album{}
result := db.GetDB().First(&album, "id = ?", id) result := db.GetDB().First(&album, "id = ?", id)
if result.Error != nil { if result.Error != nil {
return models.Album{}, result.Error return models.Album{}, result.Error
} }
return album, nil return album, nil
} }
+45 -45
View File
@@ -1,45 +1,45 @@
package services package services
import ( import (
"log" "log"
"github.com/wisplite/raster/internal/db" "github.com/wisplite/raster/internal/db"
"github.com/wisplite/raster/internal/models" "github.com/wisplite/raster/internal/models"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
func CreateUser(username string, password string) error { func CreateUser(username string, password string) error {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil { if err != nil {
log.Fatal("failed to hash password: ", err) log.Fatal("failed to hash password: ", err)
return err return err
} }
user := models.User{ user := models.User{
Username: username, Username: username,
Password: string(hashedPassword), Password: string(hashedPassword),
IsAdmin: false, IsAdmin: false,
IsActive: false, IsActive: false,
} }
result := db.GetDB().Create(&user) result := db.GetDB().Create(&user)
if result.Error != nil { if result.Error != nil {
return result.Error return result.Error
} }
return nil return nil
} }
func Login(username string, password string) (models.AccessToken, error) { func Login(username string, password string) (models.AccessToken, error) {
user := models.User{} user := models.User{}
result := db.GetDB().First(&user, "username = ?", username) result := db.GetDB().First(&user, "username = ?", username)
if result.Error != nil { if result.Error != nil {
return models.AccessToken{}, result.Error return models.AccessToken{}, result.Error
} }
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
if err != nil { if err != nil {
return models.AccessToken{}, err return models.AccessToken{}, err
} }
accessToken, err := CreateAccessToken(user.ID) accessToken, err := CreateAccessToken(user.ID)
if err != nil { if err != nil {
return models.AccessToken{}, err return models.AccessToken{}, err
} }
return accessToken, nil return accessToken, nil
} }