mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 12:17:24 +00:00
Initial commit
This commit is contained in:
254
server/api/file.go
Normal file
254
server/api/file.go
Normal file
@@ -0,0 +1,254 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gofiber/fiber/v3/log"
|
||||
"mime/multipart"
|
||||
"nysoure/server/model"
|
||||
"nysoure/server/service"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func AddFileRoutes(router fiber.Router) {
|
||||
fileGroup := router.Group("/files")
|
||||
{
|
||||
fileGroup.Post("/upload/init", initUpload)
|
||||
fileGroup.Post("/upload/block/:id/:index", uploadBlock)
|
||||
fileGroup.Post("/upload/finish/:id", finishUpload)
|
||||
fileGroup.Post("/redirect", createRedirectFile)
|
||||
fileGroup.Get("/:id", getFile)
|
||||
fileGroup.Put("/:id", updateFile)
|
||||
fileGroup.Delete("/:id", deleteFile)
|
||||
}
|
||||
}
|
||||
|
||||
// initUpload 初始化文件上传过程
|
||||
func initUpload(c fiber.Ctx) error {
|
||||
uid := c.Locals("uid").(uint)
|
||||
|
||||
type InitUploadRequest struct {
|
||||
Filename string `json:"filename"`
|
||||
Description string `json:"description"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
ResourceID uint `json:"resource_id"`
|
||||
StorageID uint `json:"storage_id"`
|
||||
}
|
||||
|
||||
var req InitUploadRequest
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
return c.JSON(model.Response[any]{
|
||||
Success: false,
|
||||
Message: "无效的请求参数",
|
||||
})
|
||||
}
|
||||
|
||||
result, err := service.CreateUploadingFile(uid, req.Filename, req.Description, req.FileSize, req.ResourceID, req.StorageID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(model.Response[*model.UploadingFileView]{
|
||||
Success: true,
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
// uploadBlock 上传文件块
|
||||
func uploadBlock(c fiber.Ctx) error {
|
||||
uid := c.Locals("uid").(uint)
|
||||
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
||||
if err != nil {
|
||||
return c.JSON(model.Response[any]{
|
||||
Success: false,
|
||||
Message: "无效的文件ID",
|
||||
})
|
||||
}
|
||||
|
||||
index, err := strconv.Atoi(c.Params("index"))
|
||||
if err != nil {
|
||||
return c.JSON(model.Response[any]{
|
||||
Success: false,
|
||||
Message: "无效的块索引",
|
||||
})
|
||||
}
|
||||
|
||||
file, err := c.Request().MultipartForm()
|
||||
if err != nil {
|
||||
return c.JSON(model.Response[any]{
|
||||
Success: false,
|
||||
Message: "无效的文件数据",
|
||||
})
|
||||
}
|
||||
|
||||
if len(file.File["block"]) == 0 {
|
||||
return c.JSON(model.Response[any]{
|
||||
Success: false,
|
||||
Message: "没有找到文件块",
|
||||
})
|
||||
}
|
||||
|
||||
fileHeader := file.File["block"][0]
|
||||
fileContent, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
log.Error("打开文件块失败: ", err)
|
||||
return c.JSON(model.Response[any]{
|
||||
Success: false,
|
||||
Message: "打开文件块失败",
|
||||
})
|
||||
}
|
||||
defer func(fileContent multipart.File) {
|
||||
_ = fileContent.Close()
|
||||
}(fileContent)
|
||||
|
||||
data := make([]byte, fileHeader.Size)
|
||||
if _, err := fileContent.Read(data); err != nil {
|
||||
log.Error("读取文件块失败: ", err)
|
||||
return c.JSON(model.Response[any]{
|
||||
Success: false,
|
||||
Message: "读取文件块失败",
|
||||
})
|
||||
}
|
||||
|
||||
if err := service.UploadBlock(uid, uint(id), index, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(model.Response[any]{
|
||||
Success: true,
|
||||
Message: fmt.Sprintf("块 %d 上传成功", index),
|
||||
})
|
||||
}
|
||||
|
||||
// finishUpload 完成文件上传
|
||||
func finishUpload(c fiber.Ctx) error {
|
||||
uid := c.Locals("uid").(uint)
|
||||
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
||||
if err != nil {
|
||||
return c.JSON(model.Response[any]{
|
||||
Success: false,
|
||||
Message: "无效的文件ID",
|
||||
})
|
||||
}
|
||||
|
||||
result, err := service.FinishUploadingFile(uid, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(model.Response[*model.FileView]{
|
||||
Success: true,
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
// createRedirectFile 创建重定向文件
|
||||
func createRedirectFile(c fiber.Ctx) error {
|
||||
uid := c.Locals("uid").(uint)
|
||||
|
||||
type CreateRedirectFileRequest struct {
|
||||
Filename string `json:"filename"`
|
||||
Description string `json:"description"`
|
||||
ResourceID uint `json:"resource_id"`
|
||||
RedirectURL string `json:"redirect_url"`
|
||||
}
|
||||
|
||||
var req CreateRedirectFileRequest
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
return c.JSON(model.Response[any]{
|
||||
Success: false,
|
||||
Message: "无效的请求参数",
|
||||
})
|
||||
}
|
||||
|
||||
result, err := service.CreateRedirectFile(uid, req.Filename, req.Description, req.ResourceID, req.RedirectURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(model.Response[*model.FileView]{
|
||||
Success: true,
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
// getFile 获取文件信息
|
||||
func getFile(c fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
||||
if err != nil {
|
||||
return c.JSON(model.Response[any]{
|
||||
Success: false,
|
||||
Message: "无效的文件ID",
|
||||
})
|
||||
}
|
||||
|
||||
file, err := service.GetFile(uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(model.Response[*model.FileView]{
|
||||
Success: true,
|
||||
Data: file,
|
||||
})
|
||||
}
|
||||
|
||||
// updateFile 更新文件信息
|
||||
func updateFile(c fiber.Ctx) error {
|
||||
uid := c.Locals("uid").(uint)
|
||||
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
||||
if err != nil {
|
||||
return c.JSON(model.Response[any]{
|
||||
Success: false,
|
||||
Message: "无效的文件ID",
|
||||
})
|
||||
}
|
||||
|
||||
type UpdateFileRequest struct {
|
||||
Filename string `json:"filename"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
var req UpdateFileRequest
|
||||
if err := c.Bind().Body(&req); err != nil {
|
||||
return c.JSON(model.Response[any]{
|
||||
Success: false,
|
||||
Message: "无效的请求参数",
|
||||
})
|
||||
}
|
||||
|
||||
result, err := service.UpdateFile(uid, uint(id), req.Filename, req.Description)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(model.Response[*model.FileView]{
|
||||
Success: true,
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
// deleteFile 删除文件
|
||||
func deleteFile(c fiber.Ctx) error {
|
||||
uid := c.Locals("uid").(uint)
|
||||
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
||||
if err != nil {
|
||||
return c.JSON(model.Response[any]{
|
||||
Success: false,
|
||||
Message: "无效的文件ID",
|
||||
})
|
||||
}
|
||||
|
||||
if err := service.DeleteFile(uid, uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(model.Response[any]{
|
||||
Success: true,
|
||||
Message: "文件删除成功",
|
||||
})
|
||||
}
|
86
server/api/image.go
Normal file
86
server/api/image.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"net/http"
|
||||
"nysoure/server/model"
|
||||
"nysoure/server/service"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func handleUploadImage(c fiber.Ctx) error {
|
||||
uid, ok := c.Locals("uid").(uint)
|
||||
if !ok {
|
||||
return model.NewUnAuthorizedError("Unauthorized")
|
||||
}
|
||||
if err := service.HavePermissionToUpload(uid); err != nil {
|
||||
return err
|
||||
}
|
||||
data := c.Body()
|
||||
contentType := http.DetectContentType(data)
|
||||
if !strings.HasPrefix(contentType, "image/") {
|
||||
return model.NewRequestError("Invalid image format")
|
||||
}
|
||||
id, err := service.CreateImage(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(model.Response[uint]{
|
||||
Success: true,
|
||||
Data: id,
|
||||
Message: "Image uploaded successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetImage(c fiber.Ctx) error {
|
||||
idStr := c.Params("id")
|
||||
if idStr == "" {
|
||||
return model.NewRequestError("Image ID is required")
|
||||
}
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
return model.NewRequestError("Invalid image ID")
|
||||
}
|
||||
image, err := service.GetImage(uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contentType := http.DetectContentType(image)
|
||||
c.Set("Content-Type", contentType)
|
||||
return c.Send(image)
|
||||
}
|
||||
|
||||
func handleDeleteImage(c fiber.Ctx) error {
|
||||
idStr := c.Params("id")
|
||||
if idStr == "" {
|
||||
return model.NewRequestError("Image ID is required")
|
||||
}
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
return model.NewRequestError("Invalid image ID")
|
||||
}
|
||||
uid, ok := c.Locals("uid").(uint)
|
||||
if !ok {
|
||||
return model.NewUnAuthorizedError("Unauthorized")
|
||||
}
|
||||
if err := service.HavePermissionToUpload(uid); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := service.DeleteImage(uint(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(model.Response[any]{
|
||||
Success: true,
|
||||
Message: "Image deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func AddImageRoutes(api fiber.Router) {
|
||||
image := api.Group("/image")
|
||||
{
|
||||
image.Put("/", handleUploadImage)
|
||||
image.Get("/:id", handleGetImage)
|
||||
image.Delete("/:id", handleDeleteImage)
|
||||
}
|
||||
}
|
139
server/api/resource.go
Normal file
139
server/api/resource.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"nysoure/server/model"
|
||||
"nysoure/server/service"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func handleCreateResource(c fiber.Ctx) error {
|
||||
var params service.ResourceCreateParams
|
||||
body := c.Body()
|
||||
err := json.Unmarshal(body, ¶ms)
|
||||
if err != nil {
|
||||
return model.NewRequestError("Invalid request body")
|
||||
}
|
||||
uid, ok := c.Locals("uid").(uint)
|
||||
if !ok {
|
||||
return model.NewUnAuthorizedError("You must be logged in to create a resource")
|
||||
}
|
||||
id, err := service.CreateResource(uid, ¶ms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(model.Response[uint]{
|
||||
Success: true,
|
||||
Data: id,
|
||||
Message: "Resource created successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetResource(c fiber.Ctx) error {
|
||||
idStr := c.Params("id")
|
||||
if idStr == "" {
|
||||
return model.NewRequestError("Resource ID is required")
|
||||
}
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
return model.NewRequestError("Invalid resource ID")
|
||||
}
|
||||
resource, err := service.GetResource(uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(model.Response[model.ResourceDetailView]{
|
||||
Success: true,
|
||||
Data: *resource,
|
||||
Message: "Resource retrieved successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func handleDeleteResource(c fiber.Ctx) error {
|
||||
idStr := c.Params("id")
|
||||
if idStr == "" {
|
||||
return model.NewRequestError("Resource ID is required")
|
||||
}
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
return model.NewRequestError("Invalid resource ID")
|
||||
}
|
||||
uid, ok := c.Locals("uid").(uint)
|
||||
if !ok {
|
||||
return model.NewUnAuthorizedError("You must be logged in to delete a resource")
|
||||
}
|
||||
err = service.DeleteResource(uid, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(model.Response[any]{
|
||||
Success: true,
|
||||
Data: nil,
|
||||
Message: "Resource deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func handleListResources(c fiber.Ctx) error {
|
||||
pageStr := c.Query("page")
|
||||
if pageStr == "" {
|
||||
pageStr = "1"
|
||||
}
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil {
|
||||
return model.NewRequestError("Invalid page number")
|
||||
}
|
||||
resources, maxPage, err := service.GetResourceList(page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resources == nil {
|
||||
resources = []model.ResourceView{}
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(model.PageResponse[model.ResourceView]{
|
||||
Success: true,
|
||||
Data: resources,
|
||||
TotalPages: maxPage,
|
||||
Message: "Resources retrieved successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func handleSearchResources(c fiber.Ctx) error {
|
||||
query := c.Query("keyword")
|
||||
if query == "" {
|
||||
return model.NewRequestError("Search query is required")
|
||||
}
|
||||
pageStr := c.Query("page")
|
||||
if pageStr == "" {
|
||||
pageStr = "1"
|
||||
}
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil {
|
||||
return model.NewRequestError("Invalid page number")
|
||||
}
|
||||
resources, totalPages, err := service.SearchResource(query, page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resources == nil {
|
||||
resources = []model.ResourceView{}
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(model.PageResponse[model.ResourceView]{
|
||||
Success: true,
|
||||
Data: resources,
|
||||
TotalPages: totalPages,
|
||||
Message: "Resources retrieved successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func AddResourceRoutes(api fiber.Router) {
|
||||
resource := api.Group("/resource")
|
||||
{
|
||||
resource.Post("/", handleCreateResource)
|
||||
resource.Get("/search", handleSearchResources)
|
||||
resource.Get("/", handleListResources)
|
||||
resource.Get("/:id", handleGetResource)
|
||||
resource.Delete("/:id", handleDeleteResource)
|
||||
}
|
||||
}
|
118
server/api/storage.go
Normal file
118
server/api/storage.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"nysoure/server/model"
|
||||
"nysoure/server/service"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func handleCreateS3Storage(c fiber.Ctx) error {
|
||||
var params service.CreateS3StorageParams
|
||||
if err := c.Bind().JSON(¶ms); err != nil {
|
||||
return model.NewRequestError("Invalid request body")
|
||||
}
|
||||
|
||||
if params.Name == "" || params.EndPoint == "" || params.AccessKeyID == "" ||
|
||||
params.SecretAccessKey == "" || params.BucketName == "" {
|
||||
return model.NewRequestError("All fields are required")
|
||||
}
|
||||
|
||||
if params.MaxSizeInMB <= 0 {
|
||||
return model.NewRequestError("Max size must be greater than 0")
|
||||
}
|
||||
|
||||
uid, ok := c.Locals("uid").(uint)
|
||||
if !ok {
|
||||
return model.NewUnAuthorizedError("You are not authorized to perform this action")
|
||||
}
|
||||
|
||||
err := service.CreateS3Storage(uid, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(model.Response[any]{
|
||||
Success: true,
|
||||
Message: "S3 storage created successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func handleCreateLocalStorage(c fiber.Ctx) error {
|
||||
var params service.CreateLocalStorageParams
|
||||
if err := c.Bind().JSON(¶ms); err != nil {
|
||||
return model.NewRequestError("Invalid request body")
|
||||
}
|
||||
|
||||
if params.Name == "" || params.Path == "" {
|
||||
return model.NewRequestError("All fields are required")
|
||||
}
|
||||
|
||||
if params.MaxSizeInMB <= 0 {
|
||||
return model.NewRequestError("Max size must be greater than 0")
|
||||
}
|
||||
|
||||
uid, ok := c.Locals("uid").(uint)
|
||||
if !ok {
|
||||
return model.NewUnAuthorizedError("You are not authorized to perform this action")
|
||||
}
|
||||
|
||||
err := service.CreateLocalStorage(uid, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).JSON(model.Response[any]{
|
||||
Success: true,
|
||||
Message: "Local storage created successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func handleListStorages(c fiber.Ctx) error {
|
||||
storages, err := service.ListStorages()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if storages == nil {
|
||||
storages = []model.StorageView{}
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(model.Response[*[]model.StorageView]{
|
||||
Success: true,
|
||||
Data: &storages,
|
||||
Message: "Storages retrieved successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func handleDeleteStorage(c fiber.Ctx) error {
|
||||
idStr := c.Params("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
return model.NewRequestError("Invalid storage ID")
|
||||
}
|
||||
|
||||
uid, ok := c.Locals("uid").(uint)
|
||||
if !ok {
|
||||
return model.NewUnAuthorizedError("You are not authorized to perform this action")
|
||||
}
|
||||
|
||||
err = service.DeleteStorage(uid, uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(model.Response[any]{
|
||||
Success: true,
|
||||
Message: "Storage deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func AddStorageRoutes(r fiber.Router) {
|
||||
s := r.Group("storage")
|
||||
s.Post("/s3", handleCreateS3Storage)
|
||||
s.Post("/local", handleCreateLocalStorage)
|
||||
s.Get("/", handleListStorages)
|
||||
s.Delete("/:id", handleDeleteStorage)
|
||||
}
|
68
server/api/tag.go
Normal file
68
server/api/tag.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"nysoure/server/model"
|
||||
"nysoure/server/service"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func handleCreateTag(c fiber.Ctx) error {
|
||||
tag := c.FormValue("name")
|
||||
if tag == "" {
|
||||
return model.NewRequestError("name is required")
|
||||
}
|
||||
t, err := service.CreateTag(tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(model.Response[model.TagView]{
|
||||
Success: true,
|
||||
Data: *t,
|
||||
Message: "Tag created successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func handleSearchTag(c fiber.Ctx) error {
|
||||
keyword := c.Query("keyword")
|
||||
if keyword == "" {
|
||||
return model.NewRequestError("Keyword is required")
|
||||
}
|
||||
tags, err := service.SearchTag(keyword)
|
||||
if tags == nil {
|
||||
tags = []model.TagView{}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(model.Response[*[]model.TagView]{
|
||||
Success: true,
|
||||
Data: &tags,
|
||||
Message: "Tags retrieved successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func handleDeleteTag(c fiber.Ctx) error {
|
||||
id, err := strconv.Atoi(c.Params("id"))
|
||||
if err != nil {
|
||||
return model.NewRequestError("Invalid tag ID")
|
||||
}
|
||||
err = service.DeleteTag(uint(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(model.Response[any]{
|
||||
Success: true,
|
||||
Data: nil,
|
||||
Message: "Tag deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func AddTagRoutes(api fiber.Router) {
|
||||
tag := api.Group("/tag")
|
||||
{
|
||||
tag.Post("/", handleCreateTag)
|
||||
tag.Get("/search", handleSearchTag)
|
||||
tag.Delete("/:id", handleDeleteTag)
|
||||
}
|
||||
}
|
282
server/api/user.go
Normal file
282
server/api/user.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"nysoure/server/model"
|
||||
"nysoure/server/service"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func handleUserRegister(c fiber.Ctx) error {
|
||||
username := c.FormValue("username")
|
||||
password := c.FormValue("password")
|
||||
if username == "" || password == "" {
|
||||
return model.NewRequestError("Username and password are required")
|
||||
}
|
||||
if len(password) < 6 {
|
||||
return model.NewRequestError("Password must be at least 6 characters long")
|
||||
}
|
||||
if len(username) < 3 {
|
||||
return model.NewRequestError("Username must be at least 3 characters long")
|
||||
}
|
||||
user, err := service.CreateUser(username, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(model.Response[model.UserViewWithToken]{
|
||||
Success: true,
|
||||
Data: user,
|
||||
Message: "User created successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func handleUserLogin(c fiber.Ctx) error {
|
||||
username := c.FormValue("username")
|
||||
password := c.FormValue("password")
|
||||
if username == "" || password == "" {
|
||||
return model.NewRequestError("Username and password are required")
|
||||
}
|
||||
user, err := service.Login(username, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(model.Response[model.UserViewWithToken]{
|
||||
Success: true,
|
||||
Data: user,
|
||||
Message: "Login successful",
|
||||
})
|
||||
}
|
||||
|
||||
func handleUserChangePassword(c fiber.Ctx) error {
|
||||
uid, ok := c.Locals("uid").(uint)
|
||||
if !ok {
|
||||
return model.NewUnAuthorizedError("Unauthorized")
|
||||
}
|
||||
oldPassword := c.FormValue("old_password")
|
||||
newPassword := c.FormValue("new_password")
|
||||
if oldPassword == "" || newPassword == "" {
|
||||
return model.NewRequestError("Old and new passwords are required")
|
||||
}
|
||||
user, err := service.ChangePassword(uid, oldPassword, newPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(model.Response[model.UserViewWithToken]{
|
||||
Success: true,
|
||||
Data: user,
|
||||
Message: "Password changed successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func handleUserChangeAvatar(c fiber.Ctx) error {
|
||||
uid, ok := c.Locals("uid").(uint)
|
||||
if !ok {
|
||||
return model.NewUnAuthorizedError("Unauthorized")
|
||||
}
|
||||
file, err := c.FormFile("avatar")
|
||||
if err != nil {
|
||||
return model.NewRequestError("Avatar file is required")
|
||||
}
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imageData, err := io.ReadAll(f)
|
||||
_ = f.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user, err := service.ChangeAvatar(uid, imageData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(model.Response[model.UserView]{
|
||||
Success: true,
|
||||
Data: user,
|
||||
Message: "Avatar changed successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetUserAvatar(c fiber.Ctx) error {
|
||||
idStr := c.Params("id")
|
||||
uid, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
return model.NewRequestError("Invalid user ID")
|
||||
}
|
||||
avatar, err := service.GetAvatar(uint(uid))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contentType := http.DetectContentType(avatar)
|
||||
c.Set("Content-Type", contentType)
|
||||
c.Set("Cache-Control", "public, max-age=31536000")
|
||||
return c.Send(avatar)
|
||||
}
|
||||
|
||||
func handleSetUserAdmin(c fiber.Ctx) error {
|
||||
adminID, ok := c.Locals("uid").(uint)
|
||||
if !ok {
|
||||
return model.NewUnAuthorizedError("Unauthorized")
|
||||
}
|
||||
|
||||
userIDStr := c.FormValue("user_id")
|
||||
if userIDStr == "" {
|
||||
return model.NewRequestError("User ID is required")
|
||||
}
|
||||
|
||||
userID, err := strconv.Atoi(userIDStr)
|
||||
if err != nil {
|
||||
return model.NewRequestError("Invalid user ID")
|
||||
}
|
||||
|
||||
isAdminStr := c.FormValue("is_admin")
|
||||
if isAdminStr == "" {
|
||||
return model.NewRequestError("is_admin parameter is required")
|
||||
}
|
||||
|
||||
isAdmin := isAdminStr == "true" || isAdminStr == "1"
|
||||
|
||||
user, err := service.SetUserAdmin(adminID, uint(userID), isAdmin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(model.Response[model.UserView]{
|
||||
Success: true,
|
||||
Data: user,
|
||||
Message: "User admin status updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func handleSetUserUploadPermission(c fiber.Ctx) error {
|
||||
adminID, ok := c.Locals("uid").(uint)
|
||||
if !ok {
|
||||
return model.NewUnAuthorizedError("Unauthorized")
|
||||
}
|
||||
|
||||
userIDStr := c.FormValue("user_id")
|
||||
if userIDStr == "" {
|
||||
return model.NewRequestError("User ID is required")
|
||||
}
|
||||
|
||||
userID, err := strconv.Atoi(userIDStr)
|
||||
if err != nil {
|
||||
return model.NewRequestError("Invalid user ID")
|
||||
}
|
||||
|
||||
canUploadStr := c.FormValue("can_upload")
|
||||
if canUploadStr == "" {
|
||||
return model.NewRequestError("can_upload parameter is required")
|
||||
}
|
||||
|
||||
canUpload := canUploadStr == "true" || canUploadStr == "1"
|
||||
|
||||
user, err := service.SetUserUploadPermission(adminID, uint(userID), canUpload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(model.Response[model.UserView]{
|
||||
Success: true,
|
||||
Data: user,
|
||||
Message: "User upload permission updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func handleListUsers(c fiber.Ctx) error {
|
||||
adminID, ok := c.Locals("uid").(uint)
|
||||
if !ok {
|
||||
return model.NewUnAuthorizedError("Unauthorized")
|
||||
}
|
||||
|
||||
pageStr := c.Query("page", "1")
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil || page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
users, totalPages, err := service.ListUsers(adminID, page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(model.PageResponse[model.UserView]{
|
||||
Success: true,
|
||||
TotalPages: totalPages,
|
||||
Data: users,
|
||||
Message: "Users retrieved successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func handleSearchUsers(c fiber.Ctx) error {
|
||||
adminID, ok := c.Locals("uid").(uint)
|
||||
if !ok {
|
||||
return model.NewUnAuthorizedError("Unauthorized")
|
||||
}
|
||||
|
||||
username := c.Query("username", "")
|
||||
if username == "" {
|
||||
return model.NewRequestError("Username search parameter is required")
|
||||
}
|
||||
|
||||
pageStr := c.Query("page", "1")
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil || page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
users, totalPages, err := service.SearchUsers(adminID, username, page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(model.PageResponse[model.UserView]{
|
||||
Success: true,
|
||||
TotalPages: totalPages,
|
||||
Data: users,
|
||||
Message: "Users found successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func handleDeleteUser(c fiber.Ctx) error {
|
||||
adminID, ok := c.Locals("uid").(uint)
|
||||
if !ok {
|
||||
return model.NewUnAuthorizedError("Unauthorized")
|
||||
}
|
||||
|
||||
userIDStr := c.FormValue("user_id")
|
||||
if userIDStr == "" {
|
||||
return model.NewRequestError("User ID is required")
|
||||
}
|
||||
|
||||
userID, err := strconv.Atoi(userIDStr)
|
||||
if err != nil {
|
||||
return model.NewRequestError("Invalid user ID")
|
||||
}
|
||||
|
||||
if err := service.DeleteUser(adminID, uint(userID)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(model.Response[any]{
|
||||
Success: true,
|
||||
Message: "User deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func AddUserRoutes(r fiber.Router) {
|
||||
u := r.Group("user")
|
||||
u.Post("/register", handleUserRegister)
|
||||
u.Post("/login", handleUserLogin)
|
||||
u.Put("/avatar", handleUserChangeAvatar)
|
||||
u.Post("/password", handleUserChangePassword)
|
||||
u.Get("/avatar/:id", handleGetUserAvatar)
|
||||
u.Post("/set_admin", handleSetUserAdmin)
|
||||
u.Post("/set_upload_permission", handleSetUserUploadPermission)
|
||||
u.Get("/list", handleListUsers)
|
||||
u.Get("/search", handleSearchUsers)
|
||||
u.Post("/delete", handleDeleteUser)
|
||||
}
|
23
server/dao/db.go
Normal file
23
server/dao/db.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"nysoure/server/model"
|
||||
)
|
||||
|
||||
var db *gorm.DB
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
db, err = gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
|
||||
if err != nil {
|
||||
panic("failed to connect database")
|
||||
}
|
||||
|
||||
_ = db.AutoMigrate(&model.User{}, &model.Resource{}, &model.Image{}, &model.Tag{}, &model.Storage{}, &model.File{}, &model.UploadingFile{}, &model.Statistic{})
|
||||
}
|
||||
|
||||
func GetDB() *gorm.DB {
|
||||
return db
|
||||
}
|
137
server/dao/file.go
Normal file
137
server/dao/file.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"gorm.io/gorm"
|
||||
"nysoure/server/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
func CreateUploadingFile(filename string, description string, fileSize int64, blockSize int64, tempPath string, resourceID, storageID, userID uint) (*model.UploadingFile, error) {
|
||||
blocksCount := (fileSize + blockSize - 1) / blockSize
|
||||
uf := &model.UploadingFile{
|
||||
Filename: filename,
|
||||
Description: description,
|
||||
TotalSize: fileSize,
|
||||
BlockSize: blockSize,
|
||||
TempPath: tempPath,
|
||||
Blocks: make(model.UploadingFileBlocks, blocksCount),
|
||||
TargetResourceID: resourceID,
|
||||
TargetStorageID: storageID,
|
||||
UserID: userID,
|
||||
}
|
||||
if err := db.Create(uf).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return uf, nil
|
||||
}
|
||||
|
||||
func GetUploadingFile(id uint) (*model.UploadingFile, error) {
|
||||
uf := &model.UploadingFile{}
|
||||
if err := db.Where("id = ?", id).First(uf).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return uf, nil
|
||||
}
|
||||
|
||||
func UpdateUploadingBlock(id uint, blockIndex int) error {
|
||||
uf := &model.UploadingFile{}
|
||||
if err := db.Where("id = ?", id).First(uf).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if blockIndex < 0 || blockIndex >= uf.BlocksCount() {
|
||||
return nil
|
||||
}
|
||||
uf.Blocks[blockIndex] = true
|
||||
return db.Save(uf).Error
|
||||
}
|
||||
|
||||
func DeleteUploadingFile(id uint) error {
|
||||
uf := &model.UploadingFile{}
|
||||
if err := db.Where("id = ?", id).First(uf).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.Delete(uf).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetUploadingFilesOlderThan(time time.Time) ([]model.UploadingFile, error) {
|
||||
var files []model.UploadingFile
|
||||
if err := db.Where("updated_at < ?", time).Find(&files).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func CreateFile(filename string, description string, resourceID uint, storageID *uint, storageKey string, redirectUrl string) (*model.File, error) {
|
||||
if storageID == nil && redirectUrl == "" {
|
||||
return nil, errors.New("storageID and redirectUrl cannot be both empty")
|
||||
}
|
||||
f := &model.File{
|
||||
Filename: filename,
|
||||
Description: description,
|
||||
ResourceID: resourceID,
|
||||
StorageID: storageID,
|
||||
RedirectUrl: redirectUrl,
|
||||
StorageKey: storageKey,
|
||||
}
|
||||
if err := db.Create(f).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func GetFile(id uint) (*model.File, error) {
|
||||
f := &model.File{}
|
||||
if err := db.Where("id = ?", id).First(f).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, model.NewNotFoundError("file not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func GetFilesByResourceID(rID uint) ([]model.File, error) {
|
||||
var files []model.File
|
||||
if err := db.Where("resource_id = ?", rID).Find(&files).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func DeleteFile(id uint) error {
|
||||
f := &model.File{}
|
||||
if err := db.Where("id = ?", id).First(f).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return model.NewNotFoundError("file not found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := db.Delete(f).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateFile(id uint, filename string, description string) (*model.File, error) {
|
||||
f := &model.File{}
|
||||
if err := db.Where("id = ?", id).First(f).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if filename != "" {
|
||||
f.Filename = filename
|
||||
}
|
||||
if description != "" {
|
||||
f.Description = description
|
||||
}
|
||||
if err := db.Save(f).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, model.NewNotFoundError("file not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
56
server/dao/image.go
Normal file
56
server/dao/image.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"nysoure/server/model"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func CreateImage(name string, width, height int) (model.Image, error) {
|
||||
// Create a new image in the database
|
||||
i := model.Image{FileName: name, Width: width, Height: height}
|
||||
if err := db.Create(&i).Error; err != nil {
|
||||
return model.Image{}, err
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func GetImageByID(id uint) (model.Image, error) {
|
||||
// Retrieve an image by its ID from the database
|
||||
var i model.Image
|
||||
if err := db.First(&i, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return model.Image{}, model.NewNotFoundError("Image not found")
|
||||
}
|
||||
return model.Image{}, err
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func DeleteImage(id uint) error {
|
||||
// Delete an image from the database
|
||||
i := model.Image{}
|
||||
i.ID = id
|
||||
if err := db.Delete(&i).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetUnusedImages() ([]model.Image, error) {
|
||||
// Retrieve all images that are not used in any post
|
||||
var images []model.Image
|
||||
oneDayAgo := time.Now().Add(-24 * time.Hour)
|
||||
if err := db.
|
||||
Where("NOT EXISTS (SELECT 1 FROM resource_images WHERE image_id = images.id)").
|
||||
Where("created_at < ?", oneDayAgo).
|
||||
Find(&images).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return images, nil
|
||||
}
|
169
server/dao/resource.go
Normal file
169
server/dao/resource.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"nysoure/server/model"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func CreateResource(r model.Resource) (model.Resource, error) {
|
||||
// Create a new resource in the database
|
||||
if err := db.Create(&r).Error; err != nil {
|
||||
return model.Resource{}, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func GetResourceByID(id uint) (model.Resource, error) {
|
||||
// Retrieve a resource by its ID from the database
|
||||
var r model.Resource
|
||||
if err := db.Preload("User").Preload("Images").Preload("Tags").Preload("Files").First(&r, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return model.Resource{}, model.NewNotFoundError("Resource not found")
|
||||
}
|
||||
return model.Resource{}, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func GetResourceList(page, pageSize int) ([]model.Resource, int, error) {
|
||||
// Retrieve a list of resources with pagination
|
||||
var resources []model.Resource
|
||||
var total int64
|
||||
|
||||
if err := db.Model(&model.Resource{}).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if err := db.Offset((page - 1) * pageSize).Limit(pageSize).Preload("User").Preload("Images").Preload("Tags").Order("created_at DESC").Find(&resources).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
totalPages := int(total) / pageSize
|
||||
|
||||
return resources, int(totalPages), nil
|
||||
}
|
||||
|
||||
func UpdateResource(r model.Resource) error {
|
||||
// Update a resource in the database
|
||||
if err := db.Save(&r).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteResource(id uint) error {
|
||||
// Delete a resource from the database
|
||||
r := model.Resource{}
|
||||
r.ID = id
|
||||
if err := db.Delete(&r).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Search(query string, page, pageSize int) ([]model.Resource, int, error) {
|
||||
query = strings.TrimSpace(query)
|
||||
keywords := strings.Split(query, " ")
|
||||
resource, err := searchWithKeyword(keywords[0])
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if len(keywords) > 1 {
|
||||
for _, keyword := range keywords[1:] {
|
||||
r := make([]model.Resource, 0, len(resource))
|
||||
for _, res := range resource {
|
||||
if strings.Contains(res.Title, keyword) {
|
||||
r = append(r, res)
|
||||
continue
|
||||
}
|
||||
ok := false
|
||||
for _, at := range res.AlternativeTitles {
|
||||
if strings.Contains(at, keyword) {
|
||||
r = append(r, res)
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
for _, tag := range res.Tags {
|
||||
if tag.Name == keyword {
|
||||
r = append(r, res)
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
resource = r
|
||||
}
|
||||
}
|
||||
|
||||
startIndex := (page - 1) * pageSize
|
||||
endIndex := startIndex + pageSize
|
||||
if startIndex > len(resource) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
if endIndex > len(resource) {
|
||||
endIndex = len(resource)
|
||||
}
|
||||
totalPages := len(resource) / pageSize
|
||||
|
||||
result := make([]model.Resource, 0, endIndex-startIndex)
|
||||
for i := startIndex; i < endIndex; i++ {
|
||||
var r model.Resource
|
||||
if err := db.Model(&r).Preload("User").Preload("Images").Preload("Tags").Where("id=?", resource[i].ID).First(&r).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
result = append(result, r)
|
||||
}
|
||||
|
||||
return result, totalPages, nil
|
||||
}
|
||||
|
||||
func searchWithKeyword(keyword string) ([]model.Resource, error) {
|
||||
if len(keyword) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if len([]rune(keyword)) < 20 {
|
||||
var tag model.Tag
|
||||
if err := db.Where("name = ?", keyword).First(&tag).Error; err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if err := db.Model(&tag).Preload("Resources").Find(&tag).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tag.Resources, nil
|
||||
}
|
||||
}
|
||||
if len([]rune(keyword)) < 80 {
|
||||
var resources []model.Resource
|
||||
if err := db.Where("title LIKE ?", "%"+keyword+"%").Or("alternative_titles LIKE ?", "%"+keyword+"%").Find(&resources).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
return nil, model.NewRequestError("Keyword too long")
|
||||
}
|
||||
|
||||
func GetResourceByTag(tagID uint, page int, pageSize int) ([]model.Resource, int, error) {
|
||||
var tag model.Tag
|
||||
var total int64
|
||||
|
||||
total = db.Model(&model.Tag{}).Where("id = ?", tagID).Association("Resources").Count()
|
||||
|
||||
if err := db.Model(&model.Tag{}).Where("id = ?", tagID).Preload("User").Preload("Resources", func(tx *gorm.DB) *gorm.DB {
|
||||
return tx.Offset((page - 1) * pageSize).Limit(pageSize)
|
||||
}).First(&tag).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
totalPages := int(total) / pageSize
|
||||
|
||||
return tag.Resources, totalPages, nil
|
||||
}
|
22
server/dao/statistic.go
Normal file
22
server/dao/statistic.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package dao
|
||||
|
||||
import "nysoure/server/model"
|
||||
|
||||
func SetStatistic(key string, value int64) error {
|
||||
statistic := &model.Statistic{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
if err := db.Save(statistic).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetStatistic(key string) int64 {
|
||||
statistic := &model.Statistic{}
|
||||
if err := db.Where("key = ?", key).First(statistic).Error; err != nil {
|
||||
return 0
|
||||
}
|
||||
return statistic.Value
|
||||
}
|
24
server/dao/storage.go
Normal file
24
server/dao/storage.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package dao
|
||||
|
||||
import "nysoure/server/model"
|
||||
|
||||
func CreateStorage(s model.Storage) (model.Storage, error) {
|
||||
err := db.Model(&s).Create(&s).Error
|
||||
return s, err
|
||||
}
|
||||
|
||||
func DeleteStorage(id uint) error {
|
||||
return db.Model(&model.Storage{}).Where("id = ?", id).Delete(&model.Storage{}).Error
|
||||
}
|
||||
|
||||
func GetStorages() ([]model.Storage, error) {
|
||||
var storages []model.Storage
|
||||
err := db.Model(&model.Storage{}).Find(&storages).Error
|
||||
return storages, err
|
||||
}
|
||||
|
||||
func GetStorage(id uint) (model.Storage, error) {
|
||||
var storage model.Storage
|
||||
err := db.Model(&model.Storage{}).Where("id = ?", id).First(&storage).Error
|
||||
return storage, err
|
||||
}
|
60
server/dao/tag.go
Normal file
60
server/dao/tag.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"nysoure/server/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func CreateTag(tag string) (model.Tag, error) {
|
||||
// Create a new tag in the database
|
||||
t := model.Tag{Name: tag}
|
||||
if err := db.Create(&t).Error; err != nil {
|
||||
return model.Tag{}, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func SearchTag(keyword string) ([]model.Tag, error) {
|
||||
// Search for a tag by its name in the database
|
||||
var t []model.Tag
|
||||
if err := db.Model(&model.Tag{}).Where("name Like ?", "%"+keyword+"%").Limit(10).Find(&t).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func DeleteTag(id uint) error {
|
||||
// Delete a tag from the database
|
||||
t := model.Tag{}
|
||||
t.ID = id
|
||||
if err := db.Delete(&t).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetTagByID(id uint) (model.Tag, error) {
|
||||
// Retrieve a tag by its ID from the database
|
||||
var t model.Tag
|
||||
if err := db.First(&t, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return model.Tag{}, model.NewNotFoundError("Tag not found")
|
||||
}
|
||||
return model.Tag{}, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func GetTagByName(name string) (model.Tag, error) {
|
||||
// Retrieve a tag by its name from the database
|
||||
var t model.Tag
|
||||
if err := db.Where("name = ?", name).First(&t).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return model.Tag{}, model.NewNotFoundError("Tag not found")
|
||||
}
|
||||
return model.Tag{}, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
120
server/dao/user.go
Normal file
120
server/dao/user.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package dao
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"gorm.io/gorm"
|
||||
"nysoure/server/model"
|
||||
)
|
||||
|
||||
func CreateUser(username string, hashedPassword []byte) (model.User, error) {
|
||||
isEmpty, err := IsUserDataBaseEmpty()
|
||||
if err != nil {
|
||||
return model.User{}, err
|
||||
}
|
||||
user := model.User{
|
||||
Username: username,
|
||||
PasswordHash: hashedPassword,
|
||||
IsAdmin: isEmpty,
|
||||
}
|
||||
exists, err := ExistsUser(username)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
if exists {
|
||||
return user, &model.RequestError{
|
||||
Message: "User already exists",
|
||||
}
|
||||
}
|
||||
if err := db.Create(&user).Error; err != nil {
|
||||
return user, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func ExistsUser(username string) (bool, error) {
|
||||
var count int64
|
||||
if err := db.Model(&model.User{}).Where("username = ?", username).Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func GetUserByUsername(username string) (model.User, error) {
|
||||
var user model.User
|
||||
if err := db.Where("username = ?", username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return user, model.NewNotFoundError("User not found")
|
||||
}
|
||||
return user, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func GetUserByID(id uint) (model.User, error) {
|
||||
var user model.User
|
||||
if err := db.First(&user, id).Error; err != nil {
|
||||
return user, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func UpdateUser(user model.User) error {
|
||||
if err := db.Save(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsUserDataBaseEmpty() (bool, error) {
|
||||
var user model.User
|
||||
if err := db.First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return true, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 获取分页用户列表
|
||||
func ListUsers(page, pageSize int) ([]model.User, int64, error) {
|
||||
var users []model.User
|
||||
var total int64
|
||||
|
||||
// 获取总数
|
||||
if err := db.Model(&model.User{}).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页获取用户
|
||||
offset := (page - 1) * pageSize
|
||||
if err := db.Offset(offset).Limit(pageSize).Order("id desc").Find(&users).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return users, total, nil
|
||||
}
|
||||
|
||||
// 根据用户名搜索用户
|
||||
func SearchUsersByUsername(username string, page, pageSize int) ([]model.User, int64, error) {
|
||||
var users []model.User
|
||||
var total int64
|
||||
|
||||
// 获取符合条件的总数
|
||||
if err := db.Model(&model.User{}).Where("username LIKE ?", "%"+username+"%").Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页获取符合条件的用户
|
||||
offset := (page - 1) * pageSize
|
||||
if err := db.Where("username LIKE ?", "%"+username+"%").Offset(offset).Limit(pageSize).Order("id desc").Find(&users).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return users, total, nil
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
func DeleteUser(id uint) error {
|
||||
return db.Delete(&model.User{}, id).Error
|
||||
}
|
69
server/middleware/error_handler.go
Normal file
69
server/middleware/error_handler.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/gofiber/fiber/v3/log"
|
||||
"nysoure/server/model"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func ErrorHandler(c fiber.Ctx) error {
|
||||
err := c.Next()
|
||||
if err != nil {
|
||||
var requestErr *model.RequestError
|
||||
var unauthorizedErr *model.UnAuthorizedError
|
||||
var notFoundErr *model.NotFoundError
|
||||
if errors.As(err, &requestErr) {
|
||||
log.Error("Request Error: ", err)
|
||||
return c.Status(fiber.StatusBadRequest).JSON(model.Response[any]{
|
||||
Success: false,
|
||||
Data: nil,
|
||||
Message: requestErr.Error(),
|
||||
})
|
||||
} else if errors.As(err, &unauthorizedErr) {
|
||||
log.Error("Unauthorized Error: ", err)
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(model.Response[any]{
|
||||
Success: false,
|
||||
Data: nil,
|
||||
Message: unauthorizedErr.Error(),
|
||||
})
|
||||
} else if errors.As(err, ¬FoundErr) {
|
||||
log.Error("Not Found Error: ", err)
|
||||
return c.Status(fiber.StatusNotFound).JSON(model.Response[any]{
|
||||
Success: false,
|
||||
Data: nil,
|
||||
Message: notFoundErr.Error(),
|
||||
})
|
||||
} else if errors.Is(err, fiber.ErrNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(model.Response[any]{
|
||||
Success: false,
|
||||
Data: nil,
|
||||
Message: "Not found",
|
||||
})
|
||||
} else if errors.Is(err, fiber.ErrMethodNotAllowed) {
|
||||
return c.Status(fiber.StatusMethodNotAllowed).JSON(model.Response[any]{
|
||||
Success: false,
|
||||
Data: nil,
|
||||
Message: "Method not allowed",
|
||||
})
|
||||
} else {
|
||||
var fiberErr *fiber.Error
|
||||
if errors.As(err, &fiberErr) {
|
||||
if fiberErr.Code == fiber.StatusNotFound {
|
||||
return c.Status(fiber.StatusNotFound).JSON(model.Response[any]{
|
||||
Success: false,
|
||||
Data: nil,
|
||||
Message: "Not found",
|
||||
})
|
||||
}
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(model.Response[any]{
|
||||
Success: false,
|
||||
Data: nil,
|
||||
Message: "Internal server error",
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
19
server/middleware/jwt_middleware.go
Normal file
19
server/middleware/jwt_middleware.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"nysoure/server/model"
|
||||
"nysoure/server/utils"
|
||||
)
|
||||
|
||||
func JwtMiddleware(c fiber.Ctx) error {
|
||||
token := c.Get("Authorization")
|
||||
if token != "" {
|
||||
id, err := utils.ParseToken(token)
|
||||
if err != nil {
|
||||
return model.NewUnAuthorizedError("Invalid token")
|
||||
}
|
||||
c.Locals("uid", id)
|
||||
}
|
||||
return c.Next()
|
||||
}
|
79
server/model/error.go
Normal file
79
server/model/error.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
type RequestError struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e *RequestError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func NewRequestError(message string) *RequestError {
|
||||
return &RequestError{
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
func IsRequestError(err error) bool {
|
||||
var requestError *RequestError
|
||||
ok := errors.As(err, &requestError)
|
||||
return ok
|
||||
}
|
||||
|
||||
type UnAuthorizedError struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e *UnAuthorizedError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func NewUnAuthorizedError(message string) *UnAuthorizedError {
|
||||
return &UnAuthorizedError{
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
func IsUnAuthorizedError(err error) bool {
|
||||
var unAuthorizedError *UnAuthorizedError
|
||||
ok := errors.As(err, &unAuthorizedError)
|
||||
return ok
|
||||
}
|
||||
|
||||
type NotFoundError struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e *NotFoundError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func NewNotFoundError(message string) *NotFoundError {
|
||||
return &NotFoundError{
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
func IsNotFoundError(err error) bool {
|
||||
var notFoundError *NotFoundError
|
||||
ok := errors.As(err, ¬FoundError)
|
||||
return ok
|
||||
}
|
||||
|
||||
type InternalServerError struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e *InternalServerError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func NewInternalServerError(message string) *InternalServerError {
|
||||
return &InternalServerError{
|
||||
Message: message,
|
||||
}
|
||||
}
|
33
server/model/file.go
Normal file
33
server/model/file.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
gorm.Model
|
||||
Filename string
|
||||
Description string
|
||||
StorageKey string
|
||||
StorageID *uint `gorm:"default:null"`
|
||||
Storage Storage
|
||||
ResourceID uint
|
||||
RedirectUrl string
|
||||
Resource Resource `gorm:"foreignKey:ResourceID"`
|
||||
UserID uint
|
||||
User User `gorm:"foreignKey:UserID"`
|
||||
}
|
||||
|
||||
type FileView struct {
|
||||
ID uint `json:"id"`
|
||||
Filename string `json:"filename"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func (f *File) ToView() *FileView {
|
||||
return &FileView{
|
||||
ID: f.ID,
|
||||
Filename: f.Filename,
|
||||
Description: f.Description,
|
||||
}
|
||||
}
|
26
server/model/image.go
Normal file
26
server/model/image.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package model
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Image struct {
|
||||
gorm.Model
|
||||
FileName string
|
||||
Width int
|
||||
Height int
|
||||
// An image can only belong to one resource, or it doesn't belong to any resource and is waiting for usage.
|
||||
Resource []Resource `gorm:"many2many:resource_images;"`
|
||||
}
|
||||
|
||||
type ImageView struct {
|
||||
ID uint `json:"id"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
|
||||
func (i *Image) ToView() ImageView {
|
||||
return ImageView{
|
||||
ID: i.ID,
|
||||
Width: i.Width,
|
||||
Height: i.Height,
|
||||
}
|
||||
}
|
89
server/model/resource.go
Normal file
89
server/model/resource.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Resource struct {
|
||||
gorm.Model
|
||||
Title string
|
||||
AlternativeTitles []string `gorm:"serializer:json"`
|
||||
Article string
|
||||
Images []Image `gorm:"many2many:resource_images;"`
|
||||
Tags []Tag `gorm:"many2many:resource_tags;"`
|
||||
Files []File `gorm:"foreignKey:ResourceID"`
|
||||
UserID uint
|
||||
User User
|
||||
}
|
||||
|
||||
type ResourceView struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Tags []TagView `json:"tags"`
|
||||
Image *ImageView `json:"image"`
|
||||
Author UserView `json:"author"`
|
||||
}
|
||||
|
||||
type ResourceDetailView struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
AlternativeTitles []string `json:"alternativeTitles"`
|
||||
Article string `json:"article"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Tags []TagView `json:"tags"`
|
||||
Images []ImageView `json:"images"`
|
||||
Files []FileView `json:"files"`
|
||||
Author UserView `json:"author"`
|
||||
}
|
||||
|
||||
func (r *Resource) ToView() ResourceView {
|
||||
tags := make([]TagView, len(r.Tags))
|
||||
for i, tag := range r.Tags {
|
||||
tags[i] = *tag.ToView()
|
||||
}
|
||||
|
||||
var image *ImageView
|
||||
if len(r.Images) > 0 {
|
||||
v := r.Images[0].ToView()
|
||||
image = &v
|
||||
}
|
||||
|
||||
return ResourceView{
|
||||
ID: r.ID,
|
||||
Title: r.Title,
|
||||
CreatedAt: r.CreatedAt,
|
||||
Tags: tags,
|
||||
Image: image,
|
||||
Author: r.User.ToView(),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resource) ToDetailView() ResourceDetailView {
|
||||
images := make([]ImageView, len(r.Images))
|
||||
for i, image := range r.Images {
|
||||
images[i] = image.ToView()
|
||||
}
|
||||
tags := make([]TagView, len(r.Tags))
|
||||
for i, tag := range r.Tags {
|
||||
tags[i] = *tag.ToView()
|
||||
}
|
||||
|
||||
files := make([]FileView, len(r.Files))
|
||||
for i, file := range r.Files {
|
||||
files[i] = *file.ToView()
|
||||
}
|
||||
return ResourceDetailView{
|
||||
ID: r.ID,
|
||||
Title: r.Title,
|
||||
AlternativeTitles: r.AlternativeTitles,
|
||||
Article: r.Article,
|
||||
CreatedAt: r.CreatedAt,
|
||||
Tags: tags,
|
||||
Images: images,
|
||||
Files: files,
|
||||
Author: r.User.ToView(),
|
||||
}
|
||||
}
|
14
server/model/response.go
Normal file
14
server/model/response.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package model
|
||||
|
||||
type Response[T any] struct {
|
||||
Success bool `json:"success"`
|
||||
Data T `json:"data,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
type PageResponse[T any] struct {
|
||||
Success bool `json:"success"`
|
||||
TotalPages int `json:"totalPages"`
|
||||
Data []T `json:"data"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
6
server/model/statistic.go
Normal file
6
server/model/statistic.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package model
|
||||
|
||||
type Statistic struct {
|
||||
Key string `gorm:"primaryKey"`
|
||||
Value int64
|
||||
}
|
35
server/model/storage.go
Normal file
35
server/model/storage.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
Type string
|
||||
Config string
|
||||
MaxSize int64
|
||||
CurrentSize int64
|
||||
}
|
||||
|
||||
type StorageView struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
MaxSize int64 `json:"maxSize"`
|
||||
CurrentSize int64 `json:"currentSize"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
func (s *Storage) ToView() StorageView {
|
||||
return StorageView{
|
||||
ID: s.ID,
|
||||
Name: s.Name,
|
||||
Type: s.Type,
|
||||
MaxSize: s.MaxSize,
|
||||
CurrentSize: s.CurrentSize,
|
||||
CreatedAt: s.CreatedAt,
|
||||
}
|
||||
}
|
21
server/model/tag.go
Normal file
21
server/model/tag.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package model
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Tag struct {
|
||||
gorm.Model
|
||||
Name string `gorm:"unique"`
|
||||
Resources []Resource `gorm:"many2many:resource_tags;"`
|
||||
}
|
||||
|
||||
type TagView struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (t *Tag) ToView() *TagView {
|
||||
return &TagView{
|
||||
ID: t.ID,
|
||||
Name: t.Name,
|
||||
}
|
||||
}
|
77
server/model/uploading_file.go
Normal file
77
server/model/uploading_file.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type UploadingFile struct {
|
||||
gorm.Model
|
||||
Filename string
|
||||
Description string
|
||||
TargetResourceID uint
|
||||
TargetStorageID uint
|
||||
UserID uint
|
||||
BlockSize int64
|
||||
TotalSize int64
|
||||
Blocks UploadingFileBlocks `gorm:"type:blob"`
|
||||
TempPath string
|
||||
Resource Resource `gorm:"foreignKey:TargetResourceID"`
|
||||
Storage Storage `gorm:"foreignKey:TargetStorageID"`
|
||||
}
|
||||
|
||||
func (uf *UploadingFile) BlocksCount() int {
|
||||
return int((uf.TotalSize + uf.BlockSize - 1) / uf.BlockSize)
|
||||
}
|
||||
|
||||
type UploadingFileBlocks []bool
|
||||
|
||||
func (ufb *UploadingFileBlocks) Scan(ctx context.Context, field *schema.Field, dst reflect.Value, dbValue interface{}) (err error) {
|
||||
data, ok := dbValue.([]byte)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
*ufb = make([]bool, len(data)*8)
|
||||
for i, b := range data {
|
||||
for j := 0; j < 8; j++ {
|
||||
(*ufb)[i*8+j] = (b>>j)&1 == 1
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ufb UploadingFileBlocks) Value(ctx context.Context, field *schema.Field, dbValue reflect.Value) (value interface{}, err error) {
|
||||
data := make([]byte, (len(ufb)+7)/8)
|
||||
for i, b := range ufb {
|
||||
if b {
|
||||
data[i/8] |= 1 << (i % 8)
|
||||
}
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
type UploadingFileView struct {
|
||||
ID uint `json:"id"`
|
||||
Filename string `json:"filename"`
|
||||
Description string `json:"description"`
|
||||
TotalSize int64 `json:"totalSize"`
|
||||
BlockSize int64 `json:"blockSize"`
|
||||
BlocksCount int `json:"blocksCount"`
|
||||
StorageID uint `json:"storageId"`
|
||||
ResourceID uint `json:"resourceId"`
|
||||
}
|
||||
|
||||
func (uf *UploadingFile) ToView() *UploadingFileView {
|
||||
return &UploadingFileView{
|
||||
ID: uf.ID,
|
||||
Filename: uf.Filename,
|
||||
Description: uf.Description,
|
||||
TotalSize: uf.TotalSize,
|
||||
BlockSize: uf.BlockSize,
|
||||
BlocksCount: uf.BlocksCount(),
|
||||
StorageID: uf.TargetStorageID,
|
||||
ResourceID: uf.TargetResourceID,
|
||||
}
|
||||
}
|
49
server/model/user.go
Normal file
49
server/model/user.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Username string
|
||||
PasswordHash []byte
|
||||
IsAdmin bool
|
||||
CanUpload bool
|
||||
AvatarVersion int
|
||||
Resources []Resource `gorm:"foreignKey:UserID"`
|
||||
}
|
||||
|
||||
type UserView struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
AvatarPath string `json:"avatar_path"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
CanUpload bool `json:"can_upload"`
|
||||
}
|
||||
|
||||
type UserViewWithToken struct {
|
||||
UserView
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func (u User) ToView() UserView {
|
||||
return UserView{
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
CreatedAt: u.CreatedAt,
|
||||
AvatarPath: fmt.Sprintf("/api/user/avatar/%d?v=%d", u.ID, u.AvatarVersion),
|
||||
IsAdmin: u.IsAdmin,
|
||||
CanUpload: u.CanUpload || u.IsAdmin,
|
||||
}
|
||||
}
|
||||
|
||||
func (u UserView) WithToken(token string) UserViewWithToken {
|
||||
return UserViewWithToken{
|
||||
UserView: u,
|
||||
Token: token,
|
||||
}
|
||||
}
|
320
server/service/file.go
Normal file
320
server/service/file.go
Normal file
@@ -0,0 +1,320 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v3/log"
|
||||
"github.com/google/uuid"
|
||||
"nysoure/server/dao"
|
||||
"nysoure/server/model"
|
||||
"nysoure/server/storage"
|
||||
"nysoure/server/utils"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
blockSize = 4 * 1024 * 1024 // 4MB
|
||||
)
|
||||
|
||||
var (
|
||||
maxUploadingSize = int64(1024 * 1024 * 1024 * 20) // TODO: make this configurable
|
||||
maxFileSize = int64(1024 * 1024 * 1024 * 8) // TODO: make this configurable
|
||||
)
|
||||
|
||||
func getUploadingSize(uid uint) int64 {
|
||||
return dao.GetStatistic("uploading_size")
|
||||
}
|
||||
|
||||
func updateUploadingSize(offset int64) {
|
||||
c := dao.GetStatistic("uploading_size")
|
||||
c += offset
|
||||
_ = dao.SetStatistic("uploading_size", c)
|
||||
}
|
||||
|
||||
func getTempDir() (string, error) {
|
||||
name := uuid.NewString()
|
||||
path := filepath.Join(utils.GetStoragePath(), "uploading", name)
|
||||
if err := os.MkdirAll(path, os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
go func() {
|
||||
// Wait for 1 minute to ensure the database is ready
|
||||
time.Sleep(time.Minute)
|
||||
for {
|
||||
oneDayAgo := time.Now().Add(-24 * time.Hour)
|
||||
oldFiles, err := dao.GetUploadingFilesOlderThan(oneDayAgo)
|
||||
if err != nil {
|
||||
log.Error("failed to get old uploading files: ", err)
|
||||
} else {
|
||||
for _, file := range oldFiles {
|
||||
if err := os.RemoveAll(file.TempPath); err != nil {
|
||||
log.Error("failed to remove temp dir: ", err)
|
||||
}
|
||||
if err := dao.DeleteUploadingFile(file.ID); err != nil {
|
||||
log.Error("failed to delete uploading file: ", err)
|
||||
}
|
||||
updateUploadingSize(-file.TotalSize)
|
||||
}
|
||||
}
|
||||
// Sleep for 1 hour
|
||||
time.Sleep(1 * time.Hour)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func CreateUploadingFile(uid uint, filename string, description string, fileSize int64, resourceID, storageID uint) (*model.UploadingFileView, error) {
|
||||
if filename == "" {
|
||||
return nil, model.NewRequestError("filename is empty")
|
||||
}
|
||||
canUpload, err := checkUserCanUpload(uid)
|
||||
if err != nil {
|
||||
log.Error("failed to check user permission: ", err)
|
||||
return nil, model.NewInternalServerError("failed to check user permission")
|
||||
}
|
||||
if !canUpload {
|
||||
return nil, model.NewUnAuthorizedError("user cannot upload file")
|
||||
}
|
||||
|
||||
if fileSize > maxFileSize {
|
||||
return nil, model.NewRequestError("file size exceeds the limit")
|
||||
}
|
||||
|
||||
currentUploadingSize := getUploadingSize(uid)
|
||||
if currentUploadingSize+fileSize > maxUploadingSize {
|
||||
log.Info("A new uploading file is rejected due to max uploading size limit")
|
||||
return nil, model.NewRequestError("server is busy, please try again later")
|
||||
}
|
||||
|
||||
tempPath, err := getTempDir()
|
||||
if err != nil {
|
||||
log.Error("failed to create temp dir: ", err)
|
||||
return nil, model.NewInternalServerError("failed to create temp dir")
|
||||
}
|
||||
uploadingFile, err := dao.CreateUploadingFile(filename, description, fileSize, blockSize, tempPath, resourceID, storageID, uid)
|
||||
if err != nil {
|
||||
log.Error("failed to create uploading file: ", err)
|
||||
_ = os.Remove(tempPath)
|
||||
return nil, model.NewInternalServerError("failed to create uploading file")
|
||||
}
|
||||
updateUploadingSize(fileSize)
|
||||
return uploadingFile.ToView(), nil
|
||||
}
|
||||
|
||||
func UploadBlock(uid uint, fid uint, index int, data []byte) error {
|
||||
uploadingFile, err := dao.GetUploadingFile(fid)
|
||||
if err != nil {
|
||||
log.Error("failed to get uploading file: ", err)
|
||||
return model.NewNotFoundError("file not found")
|
||||
}
|
||||
if uploadingFile.UserID != uid {
|
||||
return model.NewUnAuthorizedError("user cannot upload file")
|
||||
}
|
||||
if len(data) > int(uploadingFile.BlockSize) {
|
||||
return model.NewRequestError("block size exceeds the limit")
|
||||
}
|
||||
if index != uploadingFile.BlocksCount()-1 && len(data) != int(uploadingFile.BlockSize) {
|
||||
return model.NewRequestError("block size is not correct")
|
||||
}
|
||||
if index < 0 || index >= uploadingFile.BlocksCount() {
|
||||
return model.NewRequestError("block index is not correct")
|
||||
}
|
||||
if uploadingFile.Blocks[index] {
|
||||
return model.NewRequestError("block already uploaded")
|
||||
}
|
||||
|
||||
path := filepath.Join(uploadingFile.TempPath, strconv.Itoa(index))
|
||||
if err := os.WriteFile(path, data, os.ModePerm); err != nil {
|
||||
log.Error("failed to write block file: ", err)
|
||||
return model.NewInternalServerError("failed to write block file")
|
||||
}
|
||||
uploadingFile.Blocks[index] = true
|
||||
if err := dao.UpdateUploadingBlock(fid, index); err != nil {
|
||||
log.Error("failed to update uploading file: ", err)
|
||||
_ = os.Remove(path)
|
||||
return model.NewInternalServerError("failed to update uploading file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func FinishUploadingFile(uid uint, fid uint) (*model.FileView, error) {
|
||||
uploadingFile, err := dao.GetUploadingFile(fid)
|
||||
if err != nil {
|
||||
log.Error("failed to get uploading file: ", err)
|
||||
return nil, model.NewNotFoundError("file not found")
|
||||
}
|
||||
if uploadingFile.UserID != uid {
|
||||
return nil, model.NewUnAuthorizedError("user cannot upload file")
|
||||
}
|
||||
|
||||
for i := 0; i < uploadingFile.BlocksCount(); i++ {
|
||||
if !uploadingFile.Blocks[i] {
|
||||
return nil, model.NewRequestError("file is not completely uploaded")
|
||||
}
|
||||
}
|
||||
|
||||
tempRemoved := false
|
||||
|
||||
defer func() {
|
||||
if !tempRemoved {
|
||||
if err := os.RemoveAll(uploadingFile.TempPath); err != nil {
|
||||
log.Error("failed to remove temp dir: ", err)
|
||||
}
|
||||
}
|
||||
if err := dao.DeleteUploadingFile(fid); err != nil {
|
||||
log.Error("failed to delete uploading file: ", err)
|
||||
}
|
||||
updateUploadingSize(-uploadingFile.TotalSize)
|
||||
}()
|
||||
|
||||
resultFilePath := filepath.Join(utils.GetStoragePath(), uuid.NewString())
|
||||
file, err := os.OpenFile(resultFilePath, os.O_CREATE|os.O_WRONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Error("failed to open result file: ", err)
|
||||
return nil, model.NewInternalServerError("failed to finish uploading file. please re-upload")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_ = os.Remove(resultFilePath)
|
||||
}()
|
||||
|
||||
for i := 0; i < uploadingFile.BlocksCount(); i++ {
|
||||
blockPath := filepath.Join(uploadingFile.TempPath, strconv.Itoa(i))
|
||||
data, err := os.ReadFile(blockPath)
|
||||
if err != nil {
|
||||
log.Error("failed to read block file: ", err)
|
||||
_ = file.Close()
|
||||
return nil, model.NewInternalServerError("failed to finish uploading file. please re-upload")
|
||||
}
|
||||
if _, err := file.Write(data); err != nil {
|
||||
log.Error("failed to write result file: ", err)
|
||||
_ = file.Close()
|
||||
return nil, model.NewInternalServerError("failed to finish uploading file. please re-upload")
|
||||
}
|
||||
}
|
||||
|
||||
_ = file.Close()
|
||||
_ = os.RemoveAll(uploadingFile.TempPath)
|
||||
tempRemoved = true
|
||||
|
||||
s, err := dao.GetStorage(uploadingFile.TargetStorageID)
|
||||
if err != nil {
|
||||
log.Error("failed to get storage: ", err)
|
||||
return nil, model.NewInternalServerError("failed to finish uploading file. please re-upload")
|
||||
}
|
||||
|
||||
iStorage := storage.NewStorage(s)
|
||||
if iStorage == nil {
|
||||
log.Error("failed to find storage: ", err)
|
||||
return nil, model.NewInternalServerError("failed to finish uploading file. please re-upload")
|
||||
}
|
||||
|
||||
storageKey, err := iStorage.Upload(resultFilePath)
|
||||
if err != nil {
|
||||
log.Error("failed to upload file: ", err)
|
||||
return nil, model.NewInternalServerError("failed to finish uploading file. please re-upload")
|
||||
}
|
||||
|
||||
dbFile, err := dao.CreateFile(uploadingFile.Filename, uploadingFile.Description, uploadingFile.TargetResourceID, &uploadingFile.TargetStorageID, storageKey, "")
|
||||
if err != nil {
|
||||
log.Error("failed to create file in db: ", err)
|
||||
_ = iStorage.Delete(storageKey)
|
||||
return nil, model.NewInternalServerError("failed to finish uploading file. please re-upload")
|
||||
}
|
||||
|
||||
return dbFile.ToView(), nil
|
||||
}
|
||||
|
||||
func CreateRedirectFile(uid uint, filename string, description string, resourceID uint, redirectUrl string) (*model.FileView, error) {
|
||||
canUpload, err := checkUserCanUpload(uid)
|
||||
if err != nil {
|
||||
log.Error("failed to check user permission: ", err)
|
||||
return nil, model.NewInternalServerError("failed to check user permission")
|
||||
}
|
||||
if !canUpload {
|
||||
return nil, model.NewUnAuthorizedError("user cannot upload file")
|
||||
}
|
||||
|
||||
file, err := dao.CreateFile(filename, description, resourceID, nil, "", redirectUrl)
|
||||
if err != nil {
|
||||
log.Error("failed to create file in db: ", err)
|
||||
return nil, model.NewInternalServerError("failed to create file in db")
|
||||
}
|
||||
return file.ToView(), nil
|
||||
}
|
||||
|
||||
func DeleteFile(uid uint, fid uint) error {
|
||||
file, err := dao.GetFile(fid)
|
||||
if err != nil {
|
||||
log.Error("failed to get file: ", err)
|
||||
return model.NewNotFoundError("file not found")
|
||||
}
|
||||
|
||||
isAdmin, err := checkUserIsAdmin(uid)
|
||||
if err != nil {
|
||||
log.Error("failed to check user permission: ", err)
|
||||
return model.NewInternalServerError("failed to check user permission")
|
||||
}
|
||||
|
||||
if !isAdmin && file.UserID != uid {
|
||||
return model.NewUnAuthorizedError("user cannot delete file")
|
||||
}
|
||||
|
||||
iStorage := storage.NewStorage(file.Storage)
|
||||
if iStorage == nil {
|
||||
log.Error("failed to find storage: ", err)
|
||||
return model.NewInternalServerError("failed to find storage")
|
||||
}
|
||||
|
||||
if err := iStorage.Delete(file.StorageKey); err != nil {
|
||||
log.Error("failed to delete file from storage: ", err)
|
||||
return model.NewInternalServerError("failed to delete file from storage")
|
||||
}
|
||||
|
||||
if err := dao.DeleteFile(fid); err != nil {
|
||||
log.Error("failed to delete file from db: ", err)
|
||||
return model.NewInternalServerError("failed to delete file from db")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateFile(uid uint, fid uint, filename string, description string) (*model.FileView, error) {
|
||||
file, err := dao.GetFile(fid)
|
||||
if err != nil {
|
||||
log.Error("failed to get file: ", err)
|
||||
return nil, model.NewNotFoundError("file not found")
|
||||
}
|
||||
|
||||
isAdmin, err := checkUserIsAdmin(uid)
|
||||
if err != nil {
|
||||
log.Error("failed to check user permission: ", err)
|
||||
return nil, model.NewInternalServerError("failed to check user permission")
|
||||
}
|
||||
|
||||
if !isAdmin && file.UserID != uid {
|
||||
return nil, model.NewUnAuthorizedError("user cannot update file")
|
||||
}
|
||||
|
||||
file, err = dao.UpdateFile(fid, filename, description)
|
||||
if err != nil {
|
||||
log.Error("failed to update file in db: ", err)
|
||||
return nil, model.NewInternalServerError("failed to update file in db")
|
||||
}
|
||||
return file.ToView(), nil
|
||||
}
|
||||
|
||||
func GetFile(fid uint) (*model.FileView, error) {
|
||||
file, err := dao.GetFile(fid)
|
||||
if err != nil {
|
||||
log.Error("failed to get file: ", err)
|
||||
return nil, model.NewNotFoundError("file not found")
|
||||
}
|
||||
|
||||
return file.ToView(), nil
|
||||
}
|
129
server/service/image.go
Normal file
129
server/service/image.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/gofiber/fiber/v3/log"
|
||||
"github.com/google/uuid"
|
||||
"image"
|
||||
"net/http"
|
||||
"nysoure/server/dao"
|
||||
"nysoure/server/model"
|
||||
"nysoure/server/utils"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
|
||||
_ "golang.org/x/image/webp"
|
||||
|
||||
"github.com/chai2010/webp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Start a goroutine to delete unused images every hour
|
||||
go func() {
|
||||
// Wait for 1 minute to ensure the database is ready
|
||||
time.Sleep(time.Minute)
|
||||
for {
|
||||
images, err := dao.GetUnusedImages()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get unused images: %v", err)
|
||||
}
|
||||
if len(images) > 0 {
|
||||
for _, i := range images {
|
||||
err := DeleteImage(i.ID)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to delete unused image %d: %v", i.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Hour)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func CreateImage(data []byte) (uint, error) {
|
||||
if len(data) == 0 {
|
||||
return 0, model.NewRequestError("Image data is empty")
|
||||
} else if len(data) > 1024*1024*5 {
|
||||
return 0, model.NewRequestError("Image data is too large")
|
||||
}
|
||||
|
||||
imageDir := utils.GetStoragePath() + "/images/"
|
||||
if _, err := os.Stat(imageDir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(imageDir, 0755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
contentType := http.DetectContentType(data)
|
||||
if contentType != "image/jpeg" && contentType != "image/png" && contentType != "image/gif" && contentType != "image/webp" {
|
||||
return 0, model.NewRequestError("Invalid image format")
|
||||
}
|
||||
|
||||
// Reformat the image data to webp format if necessary
|
||||
img, _, err := image.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return 0, errors.New("failed to decode image data")
|
||||
}
|
||||
if img.Bounds().Dx() == 0 || img.Bounds().Dy() == 0 {
|
||||
return 0, errors.New("invalid image dimensions")
|
||||
}
|
||||
if contentType != "image/webp" {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := webp.Encode(buf, img, &webp.Options{Quality: 80}); err != nil {
|
||||
return 0, errors.New("failed to encode image data to webp format")
|
||||
}
|
||||
data = buf.Bytes()
|
||||
contentType = "image/webp"
|
||||
}
|
||||
|
||||
filename := uuid.New().String()
|
||||
if err := os.WriteFile(imageDir+filename, data, 0644); err != nil {
|
||||
return 0, errors.New("failed to save image file")
|
||||
}
|
||||
|
||||
i, err := dao.CreateImage(filename, img.Bounds().Dx(), img.Bounds().Dy())
|
||||
if err != nil {
|
||||
_ = os.Remove(imageDir + filename)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return i.ID, nil
|
||||
}
|
||||
|
||||
func GetImage(id uint) ([]byte, error) {
|
||||
i, err := dao.GetImageByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
imageDir := utils.GetStoragePath() + "/images/"
|
||||
if _, err := os.Stat(imageDir); os.IsNotExist(err) {
|
||||
return nil, model.NewNotFoundError("Image not found")
|
||||
}
|
||||
data, err := os.ReadFile(imageDir + i.FileName)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to read image file")
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func DeleteImage(id uint) error {
|
||||
i, err := dao.GetImageByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imageDir := utils.GetStoragePath() + "/images/"
|
||||
|
||||
_ = os.Remove(imageDir + i.FileName)
|
||||
|
||||
if err := dao.DeleteImage(id); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
129
server/service/resource.go
Normal file
129
server/service/resource.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"nysoure/server/dao"
|
||||
"nysoure/server/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
pageSize = 20
|
||||
)
|
||||
|
||||
type ResourceCreateParams struct {
|
||||
Title string `json:"title" binding:"required"`
|
||||
AlternativeTitles []string `json:"alternative_titles"`
|
||||
Tags []uint `json:"tags"`
|
||||
Article string `json:"article"`
|
||||
Images []uint `json:"images"`
|
||||
}
|
||||
|
||||
func CreateResource(uid uint, params *ResourceCreateParams) (uint, error) {
|
||||
canUpload, err := checkUserCanUpload(uid)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !canUpload {
|
||||
return 0, model.NewUnAuthorizedError("You have not permission to upload resources")
|
||||
}
|
||||
|
||||
images := make([]model.Image, len(params.Images))
|
||||
for i, id := range params.Images {
|
||||
images[i] = model.Image{
|
||||
Model: gorm.Model{
|
||||
ID: id,
|
||||
},
|
||||
}
|
||||
}
|
||||
tags := make([]model.Tag, len(params.Tags))
|
||||
for i, id := range params.Tags {
|
||||
tags[i] = model.Tag{
|
||||
Model: gorm.Model{
|
||||
ID: id,
|
||||
},
|
||||
}
|
||||
}
|
||||
r := model.Resource{
|
||||
Title: params.Title,
|
||||
AlternativeTitles: params.AlternativeTitles,
|
||||
Article: params.Article,
|
||||
Images: images,
|
||||
Tags: tags,
|
||||
UserID: uid,
|
||||
}
|
||||
if r, err = dao.CreateResource(r); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return r.ID, nil
|
||||
}
|
||||
|
||||
func GetResource(id uint) (*model.ResourceDetailView, error) {
|
||||
r, err := dao.GetResourceByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v := r.ToDetailView()
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
func GetResourceList(page int) ([]model.ResourceView, int, error) {
|
||||
resources, totalPages, err := dao.GetResourceList(page, pageSize)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var views []model.ResourceView
|
||||
for _, r := range resources {
|
||||
views = append(views, r.ToView())
|
||||
}
|
||||
return views, totalPages, nil
|
||||
}
|
||||
|
||||
func SearchResource(keyword string, page int) ([]model.ResourceView, int, error) {
|
||||
resources, totalPages, err := dao.Search(keyword, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var views []model.ResourceView
|
||||
for _, r := range resources {
|
||||
views = append(views, r.ToView())
|
||||
}
|
||||
return views, totalPages, nil
|
||||
}
|
||||
|
||||
func DeleteResource(uid, id uint) error {
|
||||
isAdmin, err := checkUserIsAdmin(uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isAdmin {
|
||||
r, err := dao.GetResourceByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.UserID != uid {
|
||||
return model.NewUnAuthorizedError("You have not permission to delete this resource")
|
||||
}
|
||||
}
|
||||
if err := dao.DeleteResource(id); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetResourcesWithTag(tag string, page int) ([]model.ResourceView, int, error) {
|
||||
t, err := dao.GetTagByName(tag)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
tagID := t.ID
|
||||
resources, totalPages, err := dao.GetResourceByTag(tagID, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var views []model.ResourceView
|
||||
for _, r := range resources {
|
||||
views = append(views, r.ToView())
|
||||
}
|
||||
return views, totalPages, nil
|
||||
}
|
104
server/service/storage.go
Normal file
104
server/service/storage.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v3/log"
|
||||
"nysoure/server/dao"
|
||||
"nysoure/server/model"
|
||||
"nysoure/server/storage"
|
||||
"os"
|
||||
)
|
||||
|
||||
type CreateS3StorageParams struct {
|
||||
Name string `json:"name"`
|
||||
EndPoint string `json:"endPoint"`
|
||||
AccessKeyID string `json:"accessKeyID"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
BucketName string `json:"bucketName"`
|
||||
MaxSizeInMB uint `json:"maxSizeInMB"`
|
||||
}
|
||||
|
||||
func CreateS3Storage(uid uint, params CreateS3StorageParams) error {
|
||||
isAdmin, err := checkUserIsAdmin(uid)
|
||||
if err != nil {
|
||||
log.Errorf("check user is admin failed: %s", err)
|
||||
return model.NewInternalServerError("check user is admin failed")
|
||||
}
|
||||
if !isAdmin {
|
||||
return model.NewUnAuthorizedError("only admin can create s3 storage")
|
||||
}
|
||||
s3 := storage.S3Storage{
|
||||
EndPoint: params.EndPoint,
|
||||
AccessKeyID: params.AccessKeyID,
|
||||
SecretAccessKey: params.SecretAccessKey,
|
||||
BucketName: params.BucketName,
|
||||
}
|
||||
s := model.Storage{
|
||||
Name: params.Name,
|
||||
Type: s3.Type(),
|
||||
Config: s3.ToString(),
|
||||
MaxSize: int64(params.MaxSizeInMB) * 1024 * 1024,
|
||||
}
|
||||
_, err = dao.CreateStorage(s)
|
||||
return err
|
||||
}
|
||||
|
||||
type CreateLocalStorageParams struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
MaxSizeInMB uint `json:"maxSizeInMB"`
|
||||
}
|
||||
|
||||
func CreateLocalStorage(uid uint, params CreateLocalStorageParams) error {
|
||||
isAdmin, err := checkUserIsAdmin(uid)
|
||||
if err != nil {
|
||||
log.Errorf("check user is admin failed: %s", err)
|
||||
return model.NewInternalServerError("check user is admin failed")
|
||||
}
|
||||
if !isAdmin {
|
||||
return model.NewUnAuthorizedError("only admin can create local storage")
|
||||
}
|
||||
local := storage.LocalStorage{
|
||||
Path: params.Path,
|
||||
}
|
||||
err = os.MkdirAll(params.Path, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Errorf("create local storage dir failed: %s", err)
|
||||
return model.NewInternalServerError("create local storage dir failed")
|
||||
}
|
||||
s := model.Storage{
|
||||
Name: params.Name,
|
||||
Type: local.Type(),
|
||||
Config: local.ToString(),
|
||||
MaxSize: int64(params.MaxSizeInMB) * 1024 * 1024,
|
||||
}
|
||||
_, err = dao.CreateStorage(s)
|
||||
return err
|
||||
}
|
||||
|
||||
func ListStorages() ([]model.StorageView, error) {
|
||||
storages, err := dao.GetStorages()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result []model.StorageView
|
||||
for _, s := range storages {
|
||||
result = append(result, s.ToView())
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func DeleteStorage(uid, id uint) error {
|
||||
isAdmin, err := checkUserIsAdmin(uid)
|
||||
if err != nil {
|
||||
log.Errorf("check user is admin failed: %s", err)
|
||||
return model.NewInternalServerError("check user is admin failed")
|
||||
}
|
||||
if !isAdmin {
|
||||
return model.NewUnAuthorizedError("only admin can delete storage")
|
||||
}
|
||||
err = dao.DeleteStorage(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
38
server/service/tag.go
Normal file
38
server/service/tag.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"nysoure/server/dao"
|
||||
"nysoure/server/model"
|
||||
)
|
||||
|
||||
func CreateTag(name string) (*model.TagView, error) {
|
||||
t, err := dao.CreateTag(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.ToView(), nil
|
||||
}
|
||||
|
||||
func GetTag(id uint) (*model.TagView, error) {
|
||||
t, err := dao.GetTagByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t.ToView(), nil
|
||||
}
|
||||
|
||||
func SearchTag(name string) ([]model.TagView, error) {
|
||||
tags, err := dao.SearchTag(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var tagViews []model.TagView
|
||||
for _, t := range tags {
|
||||
tagViews = append(tagViews, *t.ToView())
|
||||
}
|
||||
return tagViews, nil
|
||||
}
|
||||
|
||||
func DeleteTag(id uint) error {
|
||||
return dao.DeleteTag(id)
|
||||
}
|
263
server/service/user.go
Normal file
263
server/service/user.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"nysoure/server/dao"
|
||||
"nysoure/server/model"
|
||||
"nysoure/server/static"
|
||||
"nysoure/server/utils"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
embedAvatarCount = 1
|
||||
)
|
||||
|
||||
func CreateUser(username, password string) (model.UserViewWithToken, error) {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return model.UserViewWithToken{}, err
|
||||
}
|
||||
user, err := dao.CreateUser(username, hashedPassword)
|
||||
if err != nil {
|
||||
return model.UserViewWithToken{}, err
|
||||
}
|
||||
token, err := utils.GenerateToken(user.ID)
|
||||
if err != nil {
|
||||
return model.UserViewWithToken{}, err
|
||||
}
|
||||
return user.ToView().WithToken(token), nil
|
||||
}
|
||||
|
||||
func Login(username, password string) (model.UserViewWithToken, error) {
|
||||
user, err := dao.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
if model.IsNotFoundError(err) {
|
||||
return model.UserViewWithToken{}, model.NewRequestError("User not found")
|
||||
}
|
||||
return model.UserViewWithToken{}, err
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword(user.PasswordHash, []byte(password)); err != nil {
|
||||
return model.UserViewWithToken{}, model.NewRequestError("Invalid password")
|
||||
}
|
||||
token, err := utils.GenerateToken(user.ID)
|
||||
if err != nil {
|
||||
return model.UserViewWithToken{}, err
|
||||
}
|
||||
return user.ToView().WithToken(token), nil
|
||||
}
|
||||
|
||||
func ChangePassword(id uint, oldPassword, newPassword string) (model.UserViewWithToken, error) {
|
||||
user, err := dao.GetUserByID(id)
|
||||
if err != nil {
|
||||
return model.UserViewWithToken{}, err
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword(user.PasswordHash, []byte(oldPassword)); err != nil {
|
||||
return model.UserViewWithToken{}, err
|
||||
}
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return model.UserViewWithToken{}, err
|
||||
}
|
||||
user.PasswordHash = hashedPassword
|
||||
if err := dao.UpdateUser(user); err != nil {
|
||||
return model.UserViewWithToken{}, err
|
||||
}
|
||||
token, err := utils.GenerateToken(user.ID)
|
||||
if err != nil {
|
||||
return model.UserViewWithToken{}, err
|
||||
}
|
||||
return user.ToView().WithToken(token), nil
|
||||
}
|
||||
|
||||
func ChangeAvatar(id uint, image []byte) (model.UserView, error) {
|
||||
user, err := dao.GetUserByID(id)
|
||||
if err != nil {
|
||||
return model.UserView{}, err
|
||||
}
|
||||
if len(image) > 4*1024*1024 {
|
||||
return model.UserView{}, errors.New("image size is too large")
|
||||
}
|
||||
avatarDir := utils.GetStoragePath() + "/avatar"
|
||||
if _, err := os.Stat(avatarDir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(avatarDir, os.ModePerm); err != nil {
|
||||
return model.UserView{}, errors.New("failed to create avatar directory")
|
||||
}
|
||||
}
|
||||
avatarPath := avatarDir + "/" + strconv.Itoa(int(user.ID))
|
||||
if err := os.WriteFile(avatarPath, image, 0644); err != nil {
|
||||
return model.UserView{}, errors.New("failed to save avatar")
|
||||
}
|
||||
user.AvatarVersion++
|
||||
if err := dao.UpdateUser(user); err != nil {
|
||||
return model.UserView{}, err
|
||||
}
|
||||
return user.ToView(), nil
|
||||
}
|
||||
|
||||
func GetAvatar(id uint) ([]byte, error) {
|
||||
avatarPath := utils.GetStoragePath() + "/avatar/" + strconv.Itoa(int(id))
|
||||
if _, err := os.Stat(avatarPath); os.IsNotExist(err) {
|
||||
return getEmbedAvatar(id)
|
||||
}
|
||||
image, err := os.ReadFile(avatarPath)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to read avatar")
|
||||
}
|
||||
return image, nil
|
||||
}
|
||||
|
||||
func getEmbedAvatar(id uint) ([]byte, error) {
|
||||
fileIndex := id%embedAvatarCount + 1
|
||||
fileName := fmt.Sprintf("avatars/%d.png", fileIndex)
|
||||
return static.Static.ReadFile(fileName)
|
||||
}
|
||||
|
||||
func HavePermissionToUpload(id uint) error {
|
||||
user, err := dao.GetUserByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !user.IsAdmin && !user.CanUpload {
|
||||
return model.NewUnAuthorizedError("User does not have permission to upload")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetUserAdmin(adminID uint, targetUserID uint, isAdmin bool) (model.UserView, error) {
|
||||
if adminID == targetUserID {
|
||||
return model.UserView{}, model.NewRequestError("You cannot modify your own admin status")
|
||||
}
|
||||
|
||||
adminUser, err := dao.GetUserByID(adminID)
|
||||
if err != nil {
|
||||
return model.UserView{}, err
|
||||
}
|
||||
|
||||
if !adminUser.IsAdmin {
|
||||
return model.UserView{}, model.NewUnAuthorizedError("Only administrators can modify admin status")
|
||||
}
|
||||
|
||||
targetUser, err := dao.GetUserByID(targetUserID)
|
||||
if err != nil {
|
||||
return model.UserView{}, err
|
||||
}
|
||||
|
||||
targetUser.IsAdmin = isAdmin
|
||||
|
||||
if err := dao.UpdateUser(targetUser); err != nil {
|
||||
return model.UserView{}, err
|
||||
}
|
||||
|
||||
return targetUser.ToView(), nil
|
||||
}
|
||||
|
||||
func SetUserUploadPermission(adminID uint, targetUserID uint, canUpload bool) (model.UserView, error) {
|
||||
adminUser, err := dao.GetUserByID(adminID)
|
||||
if err != nil {
|
||||
return model.UserView{}, err
|
||||
}
|
||||
|
||||
if !adminUser.IsAdmin {
|
||||
return model.UserView{}, model.NewUnAuthorizedError("Only administrators can modify upload permissions")
|
||||
}
|
||||
|
||||
targetUser, err := dao.GetUserByID(targetUserID)
|
||||
if err != nil {
|
||||
return model.UserView{}, err
|
||||
}
|
||||
|
||||
targetUser.CanUpload = canUpload
|
||||
|
||||
if err := dao.UpdateUser(targetUser); err != nil {
|
||||
return model.UserView{}, err
|
||||
}
|
||||
|
||||
return targetUser.ToView(), nil
|
||||
}
|
||||
|
||||
func ListUsers(adminID uint, page int) ([]model.UserView, int, error) {
|
||||
admin, err := dao.GetUserByID(adminID)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if !admin.IsAdmin {
|
||||
return nil, 0, model.NewUnAuthorizedError("Only administrators can list users")
|
||||
}
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
pageSize := 10
|
||||
|
||||
users, total, err := dao.ListUsers(page, pageSize)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
userViews := make([]model.UserView, len(users))
|
||||
for i, user := range users {
|
||||
userViews[i] = user.ToView()
|
||||
}
|
||||
|
||||
totalPages := int((total + int64(pageSize) - 1) / int64(pageSize))
|
||||
return userViews, totalPages, nil
|
||||
}
|
||||
|
||||
func SearchUsers(adminID uint, username string, page int) ([]model.UserView, int, error) {
|
||||
admin, err := dao.GetUserByID(adminID)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
if !admin.IsAdmin {
|
||||
return nil, 0, model.NewUnAuthorizedError("Only administrators can search users")
|
||||
}
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
pageSize := 10
|
||||
|
||||
users, total, err := dao.SearchUsersByUsername(username, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
userViews := make([]model.UserView, len(users))
|
||||
for i, user := range users {
|
||||
userViews[i] = user.ToView()
|
||||
}
|
||||
|
||||
totalPages := int((total + int64(pageSize) - 1) / int64(pageSize))
|
||||
return userViews, totalPages, nil
|
||||
}
|
||||
|
||||
func DeleteUser(adminID uint, targetUserID uint) error {
|
||||
admin, err := dao.GetUserByID(adminID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !admin.IsAdmin {
|
||||
return model.NewUnAuthorizedError("Only administrators can delete users")
|
||||
}
|
||||
|
||||
// Check if user is trying to delete themselves
|
||||
if adminID == targetUserID {
|
||||
return model.NewRequestError("You cannot delete your own account")
|
||||
}
|
||||
|
||||
// Check if target user exists
|
||||
_, err = dao.GetUserByID(targetUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dao.DeleteUser(targetUserID)
|
||||
}
|
19
server/service/utils.go
Normal file
19
server/service/utils.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package service
|
||||
|
||||
import "nysoure/server/dao"
|
||||
|
||||
func checkUserCanUpload(uid uint) (bool, error) {
|
||||
user, err := dao.GetUserByID(uid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return user.IsAdmin || user.CanUpload, nil
|
||||
}
|
||||
|
||||
func checkUserIsAdmin(uid uint) (bool, error) {
|
||||
user, err := dao.GetUserByID(uid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return user.IsAdmin, nil
|
||||
}
|
BIN
server/static/avatars/1.png
Normal file
BIN
server/static/avatars/1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 382 KiB |
6
server/static/static.go
Normal file
6
server/static/static.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package static
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed *
|
||||
var Static embed.FS
|
61
server/storage/local.go
Normal file
61
server/storage/local.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type LocalStorage struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
func (s *LocalStorage) Upload(filePath string) (string, error) {
|
||||
id := uuid.New().String()
|
||||
inputPath := s.Path + "/" + id
|
||||
input, err := os.OpenFile(inputPath, os.O_RDWR|os.O_CREATE, 0755)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer input.Close()
|
||||
output, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer output.Close()
|
||||
_, err = io.Copy(input, output)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (s *LocalStorage) Download(storageKey string) (string, error) {
|
||||
path := s.Path + "/" + storageKey
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return "", ErrFileUnavailable
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func (s *LocalStorage) Delete(storageKey string) error {
|
||||
path := s.Path + "/" + storageKey
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
func (s *LocalStorage) ToString() string {
|
||||
return s.Path
|
||||
}
|
||||
|
||||
func (s *LocalStorage) FromString(config string) error {
|
||||
s.Path = config
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LocalStorage) Type() string {
|
||||
return "local"
|
||||
}
|
52
server/storage/s3.go
Normal file
52
server/storage/s3.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type S3Storage struct {
|
||||
EndPoint string
|
||||
AccessKeyID string
|
||||
SecretAccessKey string
|
||||
BucketName string
|
||||
}
|
||||
|
||||
func (s *S3Storage) Upload(filePath string) (string, error) {
|
||||
// TODO: Implement S3 upload logic here
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (s *S3Storage) Download(storageKey string) (string, error) {
|
||||
// TODO: Implement S3 download logic here
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (s *S3Storage) Delete(storageKey string) error {
|
||||
// TODO: Implement S3 delete logic here
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *S3Storage) ToString() string {
|
||||
data, _ := json.Marshal(s)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func (s *S3Storage) FromString(config string) error {
|
||||
var s3Config S3Storage
|
||||
if err := json.Unmarshal([]byte(config), &s3Config); err != nil {
|
||||
return err
|
||||
}
|
||||
s.EndPoint = s3Config.EndPoint
|
||||
s.AccessKeyID = s3Config.AccessKeyID
|
||||
s.SecretAccessKey = s3Config.SecretAccessKey
|
||||
s.BucketName = s3Config.BucketName
|
||||
if s.EndPoint == "" || s.AccessKeyID == "" || s.SecretAccessKey == "" || s.BucketName == "" {
|
||||
return errors.New("invalid S3 configuration")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *S3Storage) Type() string {
|
||||
return "s3"
|
||||
}
|
48
server/storage/storage.go
Normal file
48
server/storage/storage.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"nysoure/server/model"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrFileUnavailable is returned when the file is unavailable.
|
||||
// When this error is returned, it is required to delete the file info from the database.
|
||||
ErrFileUnavailable = errors.New("file unavailable")
|
||||
)
|
||||
|
||||
type IStorage interface {
|
||||
// Upload uploads a file to the storage and returns the storage key.
|
||||
Upload(filePath string) (string, error)
|
||||
// Download return the download url of the file with the given storage key.
|
||||
Download(storageKey string) (string, error)
|
||||
// Delete deletes the file with the given storage key.
|
||||
Delete(storageKey string) error
|
||||
// ToString returns the storage configuration as a string.
|
||||
ToString() string
|
||||
// FromString initializes the storage configuration from a string.
|
||||
FromString(config string) error
|
||||
// Type returns the type of the storage.
|
||||
Type() string
|
||||
}
|
||||
|
||||
func NewStorage(s model.Storage) IStorage {
|
||||
switch s.Type {
|
||||
case "s3":
|
||||
r := S3Storage{}
|
||||
err := r.FromString(s.Config)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &r
|
||||
|
||||
case "local":
|
||||
r := LocalStorage{}
|
||||
err := r.FromString(s.Config)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &r
|
||||
}
|
||||
return nil
|
||||
}
|
62
server/utils/jwt.go
Normal file
62
server/utils/jwt.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"math/rand"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
key []byte
|
||||
)
|
||||
|
||||
func init() {
|
||||
secretFilePath := GetStoragePath() + "/jwt_secret.key"
|
||||
secret, err := os.ReadFile(secretFilePath)
|
||||
if err != nil {
|
||||
key = secret
|
||||
} else {
|
||||
// Initialize the key with a random value
|
||||
chars := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
key = make([]byte, 32)
|
||||
for i := range key {
|
||||
r := rand.Intn(len(chars))
|
||||
key[i] = byte(chars[r])
|
||||
}
|
||||
err = os.WriteFile(secretFilePath, key, 0644)
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateToken(userID uint) (string, error) {
|
||||
t := jwt.NewWithClaims(jwt.SigningMethodHS256,
|
||||
jwt.MapClaims{
|
||||
"id": userID,
|
||||
"exp": time.Now().Add(7 * 24 * time.Hour).Unix(),
|
||||
})
|
||||
s, err := t.SignedString(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func ParseToken(token string) (uint, error) {
|
||||
t, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
|
||||
return key, nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if claims, ok := t.Claims.(jwt.MapClaims); ok && t.Valid {
|
||||
id := uint(claims["id"].(float64))
|
||||
expF := claims["exp"].(float64)
|
||||
exp := time.Unix(int64(expF), 0)
|
||||
if time.Now().After(exp) {
|
||||
return 0, errors.New("token expired")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
return 0, errors.New("invalid token")
|
||||
}
|
26
server/utils/storage.go
Normal file
26
server/utils/storage.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var path string
|
||||
|
||||
func GetStoragePath() string {
|
||||
if path != "" {
|
||||
return path
|
||||
}
|
||||
if runtime.GOOS == "linux" {
|
||||
path = "/var/lib/nysoure"
|
||||
} else {
|
||||
userDir, _ := os.UserHomeDir()
|
||||
path = userDir + "/.nysoure"
|
||||
}
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(path, os.ModePerm); err != nil {
|
||||
panic("failed to create storage directory")
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
Reference in New Issue
Block a user