Initial commit

This commit is contained in:
2025-05-11 20:32:14 +08:00
commit d97247159f
80 changed files with 13013 additions and 0 deletions

254
server/api/file.go Normal file
View 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
View 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
View 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, &params)
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, &params)
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
View 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(&params); 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(&params); 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
View 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
View 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)
}