mirror of
https://github.com/wisplite/raster.git
synced 2026-05-01 06:32:44 -05:00
a commit that changes nothing?
This commit is contained in:
Vendored
+20
-20
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user