Initial commit

This commit is contained in:
Artem Mamonov
2025-02-06 02:36:10 +01:00
commit acf9b43671
24 changed files with 1946 additions and 0 deletions

View File

@@ -0,0 +1,353 @@
package controller
import (
"database/sql"
"fmt"
"github.com/gin-gonic/gin"
"image/jpeg"
"net/http"
"path/filepath"
"photodisk/internal/albums"
"photodisk/internal/auth"
"photodisk/internal/config"
"photodisk/internal/fs"
"photodisk/internal/watermark"
"time"
)
// Login
type LoginRequest struct {
Username string `form:"username" json:"username" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
func Login(c *gin.Context) {
var req LoginRequest
if err := c.Bind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user, err := auth.Login(req.Username, req.Password)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
expiresAt := time.Now().Add(config.Get().SessionTtl)
sessionId, err := auth.CreateSession(user.Id, expiresAt)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
return
}
cookie := &http.Cookie{
Name: "session_id",
Value: sessionId,
Expires: expiresAt,
Path: "/",
}
http.SetCookie(c.Writer, cookie)
c.JSON(http.StatusOK, gin.H{"session": sessionId})
}
// Create
type CreateAlbumRequest struct {
Name string `form:"name" binding:"required"`
Password string `form:"password"`
}
type CreateAlbumResponse struct {
Id string `json:"id"`
}
func CreateAlbum(c *gin.Context) {
var req CreateAlbumRequest
if err := c.Bind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
id := albums.GenerateAlbumId()
if err := albums.CreateAlbum(albums.Album{
ID: id,
Name: req.Name,
Password: req.Password,
}); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, CreateAlbumResponse{Id: id})
}
// Delete
type DeleteAlbumRequest struct {
Id string `form:"id" binding:"required"`
}
type DeleteAlbumResponse struct {
}
func DeleteAlbum(c *gin.Context) {
var req DeleteAlbumRequest
if err := c.Bind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := albums.DeleteAlbum(req.Id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, DeleteAlbumResponse{})
}
// List
type ListAlbumsRequest struct {
}
type ListAlbumsResponse struct {
Albums []albums.Album `json:"albums"`
}
func ListAlbums(c *gin.Context) {
var req ListAlbumsRequest
if err := c.Bind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
list, err := albums.ListAlbums()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, ListAlbumsResponse{Albums: list})
}
// List Images
type ListImagesRequest struct {
Password string `form:"password"`
}
type ListImagesResponse struct {
Images []string `json:"images"`
}
func ListImages(c *gin.Context) {
id := c.Param("id")
var req ListImagesRequest
if err := c.Bind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
album, err := albums.GetAlbum(id, req.Password)
if err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return
}
files, err := fs.ListFiles(album.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var images []string
for _, file := range files {
suffix := ""
if filepath.Ext(file) != ".mp4" {
suffix = "?width=200"
}
images = append(images, fmt.Sprintf("/albums/%s/%s%s", album.ID, file, suffix))
}
c.JSON(http.StatusOK, ListImagesResponse{Images: images})
}
// Get Album
type GetAlbumRequest struct {
Id string `form:"id" binding:"required"`
Password string `form:"password"`
}
type GetAlbumResponse struct {
Album albums.Album `json:"album"`
}
func GetAlbum(c *gin.Context) {
var req GetAlbumRequest
if err := c.Bind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
album, err := albums.GetAlbum(req.Id, req.Password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, GetAlbumResponse{Album: album})
}
// Update Album
type UpdateAlbumRequest struct {
Id string `form:"id" binding:"required"`
Name string `form:"name" binding:"required"`
Password string `form:"password"`
IsActive bool `form:"is_active"`
}
type UpdateAlbumResponse struct {
Album albums.Album `json:"album"`
}
func UpdateAlbum(c *gin.Context) {
var req UpdateAlbumRequest
if err := c.Bind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
album, err := albums.GetAlbum(req.Id, req.Password)
if err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return
}
album.Name = req.Name
album.IsActive = req.IsActive
if err := albums.UpdateAlbum(album); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, UpdateAlbumResponse{Album: album})
}
// Serve Image
type ServeImageRequest struct {
Width int `form:"width"`
}
func ServeImage(c *gin.Context) {
albumID := c.Param("id")
fileName := c.Param("image")
if albumID == "" || fileName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "album and file query parameters are required"})
return
}
var req ServeImageRequest
if err := c.Bind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// just to check access
album, err := albums.GetAlbum(albumID, "")
if err != nil {
c.JSON(http.StatusForbidden, gin.H{"error": err})
return
}
path, exists := fs.FileExists(album.ID, fileName)
if exists {
/*
// Cache control
data := []byte(time.Now().String())
etag := fmt.Sprintf("%x", md5.Sum(data))
c.Header("Cache-Control", "public, max-age=3600")
c.Header("ETag", etag)
if match := c.GetHeader("If-None-Match"); match != "" {
if strings.Contains(match, etag) {
c.Status(http.StatusNotModified)
return
}
}
*/
if album.Watermarked && fs.IsImageFile(fileName) {
watermarkedImg, err := watermark.AddWatermark(path, config.Get().Watermark)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.Header("Content-Type", "image/jpeg")
// Resize image
if req.Width > 0 {
watermarkedImg = watermark.Resize(watermarkedImg, req.Width)
}
jpeg.Encode(c.Writer, watermarkedImg, nil)
return
}
c.File(path)
return
}
c.JSON(http.StatusNotFound, gin.H{"error": "File not found"})
}
// Upload Image
func UploadImage(c *gin.Context) {
albumID := c.Param("id")
// Parse the multipart form
err := c.Request.ParseMultipartForm(10 << 20) // Max size: 10MB
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Check if the album exists
_, err = albums.GetAlbum(albumID, "") // FIXME: add password support
if err != nil {
if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "Album not found"})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
return
}
files := c.Request.MultipartForm.File["files[]"]
for _, fileHeader := range files {
file, err := fileHeader.Open()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
file.Close()
return
}
if err := fs.CreateFile(albumID, fileHeader.Filename, file); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
file.Close()
return
}
file.Close()
}
c.JSON(http.StatusOK, gin.H{"message": "Images uploaded successfully"})
}

View File

@@ -0,0 +1,20 @@
package controller
import (
"github.com/gin-gonic/gin"
"log"
"photodisk/internal/auth"
)
func SetUser(c *gin.Context, user auth.User) {
c.Set("user", user)
}
func GetUser(c *gin.Context) auth.User {
user, ok := c.Get("user")
if !ok {
log.Fatal("user not found in context")
}
return user.(auth.User)
}