mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 12:17:24 +00:00
Initial commit
This commit is contained in:
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
|
||||
}
|
Reference in New Issue
Block a user