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.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
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user