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.formatTool": "goimports",
"go.lintOnSave": "package",
"go.vetOnSave": "package",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "never",
"source.fixAll": "never"
},
"[go]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "never",
"source.fixAll": "never"
}
},
"go.buildOnSave": "off",
"go.lintTool": "golint",
"go.toolsManagement.autoUpdate": true
{
"go.useLanguageServer": true,
"go.formatTool": "goimports",
"go.lintOnSave": "package",
"go.vetOnSave": "package",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "never",
"source.fixAll": "never"
},
"[go]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "never",
"source.fixAll": "never"
}
},
"go.buildOnSave": "off",
"go.lintTool": "golint",
"go.toolsManagement.autoUpdate": true
}
+45 -45
View File
@@ -1,46 +1,46 @@
## 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.
## 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 wanted a solution that was:
- Lightweight and easy to run
- Simple, clean UI
- Albums, content tags, and descriptions
- Public albums for showcasing work
- Private shared albums with multi-user uploads
- Comments and likes in shared albums
- Proper panorama support
- Modern tech stack
## 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.
Backend (API):
```bash
cd backend
go run ./cmd/server
# Server runs on http://localhost:8080
# A SQLite database (raster.db) will be created automatically in the backend directory.
```
Frontend (web app):
```bash
cd frontend
npm i
npm run dev
# App runs on http://localhost:5173
```
## Project structure
- `backend/`: Go API using Gin + GORM (SQLite default)
- `frontend/`: React app (Vite, Tailwind)
## Status
In early development. Expect rapid updates.
## Contributing
## 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.
## 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 wanted a solution that was:
- Lightweight and easy to run
- Simple, clean UI
- Albums, content tags, and descriptions
- Public albums for showcasing work
- Private shared albums with multi-user uploads
- Comments and likes in shared albums
- Proper panorama support
- Modern tech stack
## 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.
Backend (API):
```bash
cd backend
go run ./cmd/server
# Server runs on http://localhost:8080
# A SQLite database (raster.db) will be created automatically in the backend directory.
```
Frontend (web app):
```bash
cd frontend
npm i
npm run dev
# App runs on http://localhost:5173
```
## Project structure
- `backend/`: Go API using Gin + GORM (SQLite default)
- `frontend/`: React app (Vite, Tailwind)
## Status
In early development. Expect rapid updates.
## Contributing
Issues and PRs are welcome.
+21 -21
View File
@@ -1,21 +1,21 @@
package main
import (
"log"
"github.com/gin-gonic/gin"
"github.com/wisplite/raster/internal/db"
"github.com/wisplite/raster/internal/routes"
)
func main() {
if !db.Init() {
log.Fatal("failed to initialize database")
}
r := gin.Default()
routes.RegisterRoutes(r)
r.Run(":8080")
}
package main
import (
"log"
"github.com/gin-gonic/gin"
"github.com/wisplite/raster/internal/db"
"github.com/wisplite/raster/internal/routes"
)
func main() {
if !db.Init() {
log.Fatal("failed to initialize database")
}
r := gin.Default()
routes.RegisterRoutes(r)
r.Run(":8080")
}
+38 -38
View File
@@ -1,38 +1,38 @@
package db
import (
"log"
"github.com/glebarez/sqlite"
"github.com/wisplite/raster/internal/models"
"gorm.io/gorm"
)
var db *gorm.DB
func Init() bool {
database, err := gorm.Open(sqlite.Open("raster.db"), &gorm.Config{})
if err != nil {
log.Fatal("failed to connect database: ", err)
return false
}
// Run migrations
err = database.AutoMigrate(
&models.Album{},
&models.User{},
&models.AccessToken{},
)
if err != nil {
log.Fatal("failed to migrate database: ", err)
return false
}
db = database
return true
}
func GetDB() *gorm.DB {
return db
}
package db
import (
"log"
"github.com/glebarez/sqlite"
"github.com/wisplite/raster/internal/models"
"gorm.io/gorm"
)
var db *gorm.DB
func Init() bool {
database, err := gorm.Open(sqlite.Open("raster.db"), &gorm.Config{})
if err != nil {
log.Fatal("failed to connect database: ", err)
return false
}
// Run migrations
err = database.AutoMigrate(
&models.Album{},
&models.User{},
&models.AccessToken{},
)
if err != nil {
log.Fatal("failed to migrate database: ", err)
return false
}
db = database
return true
}
func GetDB() *gorm.DB {
return db
}
+9 -9
View File
@@ -1,9 +1,9 @@
package models
import "time"
type AccessToken struct {
Token string `gorm:"primaryKey"`
UserID string `gorm:"not null"`
Expires time.Time `gorm:"not null"`
}
package models
import "time"
type AccessToken struct {
Token string `gorm:"primaryKey"`
UserID string `gorm:"not null"`
Expires time.Time `gorm:"not null"`
}
+17 -17
View File
@@ -1,17 +1,17 @@
package models
import (
"time"
"gorm.io/datatypes"
)
type Album struct {
ID string `gorm:"primaryKey"`
Title string `gorm:"not null"`
Description string `gorm:"not null"`
Tags datatypes.JSON `gorm:"type:json"`
Private bool `gorm:"not null"`
CreatedAt time.Time
UpdatedAt time.Time
}
package models
import (
"time"
"gorm.io/datatypes"
)
type Album struct {
ID string `gorm:"primaryKey"`
Title string `gorm:"not null"`
Description string `gorm:"not null"`
Tags datatypes.JSON `gorm:"type:json"`
Private bool `gorm:"not null"`
CreatedAt time.Time
UpdatedAt time.Time
}
+13 -13
View File
@@ -1,13 +1,13 @@
package models
import "time"
type User struct {
ID string `gorm:"primaryKey"`
Username string `gorm:"not null"`
Password string `gorm:"not null"`
IsAdmin bool `gorm:"not null"`
IsActive bool `gorm:"not null"`
CreatedAt time.Time
UpdatedAt time.Time
}
package models
import "time"
type User struct {
ID string `gorm:"primaryKey"`
Username string `gorm:"not null"`
Password string `gorm:"not null"`
IsAdmin bool `gorm:"not null"`
IsActive bool `gorm:"not null"`
CreatedAt time.Time
UpdatedAt time.Time
}
+20 -20
View File
@@ -1,20 +1,20 @@
package routes
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/wisplite/raster/internal/services"
)
func RegisterAlbumRoutes(rg *gin.RouterGroup) {
album := rg.Group("/albums")
album.GET("/getPublic", func(c *gin.Context) {
albums, err := services.GetPublicAlbums()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, albums)
})
}
package routes
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/wisplite/raster/internal/services"
)
func RegisterAlbumRoutes(rg *gin.RouterGroup) {
album := rg.Group("/albums")
album.GET("/getPublic", func(c *gin.Context) {
albums, err := services.GetPublicAlbums()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, albums)
})
}
+8 -8
View File
@@ -1,8 +1,8 @@
package routes
import "github.com/gin-gonic/gin"
func RegisterRoutes(r *gin.Engine) {
rg := r.Group("/api")
RegisterAlbumRoutes(rg)
}
package routes
import "github.com/gin-gonic/gin"
func RegisterRoutes(r *gin.Engine) {
rg := r.Group("/api")
RegisterAlbumRoutes(rg)
}
+32 -32
View File
@@ -1,32 +1,32 @@
package routes
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/wisplite/raster/internal/services"
)
func RegisterUserRoutes(rg *gin.RouterGroup) {
user := rg.Group("/user")
user.POST("/createUser", func(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
err := services.CreateUser(username, password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "User created successfully"})
})
user.POST("/login", func(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
accessToken, err := services.Login(username, password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Login successful", "accessToken": accessToken.Token})
})
}
package routes
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/wisplite/raster/internal/services"
)
func RegisterUserRoutes(rg *gin.RouterGroup) {
user := rg.Group("/user")
user.POST("/createUser", func(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
err := services.CreateUser(username, password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "User created successfully"})
})
user.POST("/login", func(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
accessToken, err := services.Login(username, password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Login successful", "accessToken": accessToken.Token})
})
}
+24 -24
View File
@@ -1,24 +1,24 @@
package services
import (
"time"
"github.com/google/uuid"
"github.com/wisplite/raster/internal/db"
"github.com/wisplite/raster/internal/models"
)
func CreateAccessToken(userID string) (models.AccessToken, error) {
token := uuid.New().String()
expires := time.Now().Add(time.Hour * 24 * 30)
accessToken := models.AccessToken{
Token: token,
UserID: userID,
Expires: expires,
}
result := db.GetDB().Create(&accessToken)
if result.Error != nil {
return models.AccessToken{}, result.Error
}
return accessToken, nil
}
package services
import (
"time"
"github.com/google/uuid"
"github.com/wisplite/raster/internal/db"
"github.com/wisplite/raster/internal/models"
)
func CreateAccessToken(userID string) (models.AccessToken, error) {
token := uuid.New().String()
expires := time.Now().Add(time.Hour * 24 * 30)
accessToken := models.AccessToken{
Token: token,
UserID: userID,
Expires: expires,
}
result := db.GetDB().Create(&accessToken)
if result.Error != nil {
return models.AccessToken{}, result.Error
}
return accessToken, nil
}
+25 -25
View File
@@ -1,25 +1,25 @@
package services
import (
"github.com/wisplite/raster/internal/db"
"github.com/wisplite/raster/internal/models"
)
func GetPublicAlbums() ([]models.Album, error) {
albums := []models.Album{}
result := db.GetDB().Where("private = ?", false).Find(&albums)
if result.Error != nil {
return []models.Album{}, result.Error
}
return albums, nil
}
func GetAlbum(id string, authToken string) (models.Album, error) {
// TODO: Add authentication
album := models.Album{}
result := db.GetDB().First(&album, "id = ?", id)
if result.Error != nil {
return models.Album{}, result.Error
}
return album, nil
}
package services
import (
"github.com/wisplite/raster/internal/db"
"github.com/wisplite/raster/internal/models"
)
func GetPublicAlbums() ([]models.Album, error) {
albums := []models.Album{}
result := db.GetDB().Where("private = ?", false).Find(&albums)
if result.Error != nil {
return []models.Album{}, result.Error
}
return albums, nil
}
func GetAlbum(id string, authToken string) (models.Album, error) {
// TODO: Add authentication
album := models.Album{}
result := db.GetDB().First(&album, "id = ?", id)
if result.Error != nil {
return models.Album{}, result.Error
}
return album, nil
}
+45 -45
View File
@@ -1,45 +1,45 @@
package services
import (
"log"
"github.com/wisplite/raster/internal/db"
"github.com/wisplite/raster/internal/models"
"golang.org/x/crypto/bcrypt"
)
func CreateUser(username string, password string) error {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
log.Fatal("failed to hash password: ", err)
return err
}
user := models.User{
Username: username,
Password: string(hashedPassword),
IsAdmin: false,
IsActive: false,
}
result := db.GetDB().Create(&user)
if result.Error != nil {
return result.Error
}
return nil
}
func Login(username string, password string) (models.AccessToken, error) {
user := models.User{}
result := db.GetDB().First(&user, "username = ?", username)
if result.Error != nil {
return models.AccessToken{}, result.Error
}
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
if err != nil {
return models.AccessToken{}, err
}
accessToken, err := CreateAccessToken(user.ID)
if err != nil {
return models.AccessToken{}, err
}
return accessToken, nil
}
package services
import (
"log"
"github.com/wisplite/raster/internal/db"
"github.com/wisplite/raster/internal/models"
"golang.org/x/crypto/bcrypt"
)
func CreateUser(username string, password string) error {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
log.Fatal("failed to hash password: ", err)
return err
}
user := models.User{
Username: username,
Password: string(hashedPassword),
IsAdmin: false,
IsActive: false,
}
result := db.GetDB().Create(&user)
if result.Error != nil {
return result.Error
}
return nil
}
func Login(username string, password string) (models.AccessToken, error) {
user := models.User{}
result := db.GetDB().First(&user, "username = ?", username)
if result.Error != nil {
return models.AccessToken{}, result.Error
}
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
if err != nil {
return models.AccessToken{}, err
}
accessToken, err := CreateAccessToken(user.ID)
if err != nil {
return models.AccessToken{}, err
}
return accessToken, nil
}