mirror of
https://github.com/usememos/memos.git
synced 2024-09-20 14:35:54 +08:00
refactor: migrate storage to apiv1 (#1890)
* refactor: migrate storage to apiv1 * chore: update * chore: update * chore: update
This commit is contained in:
parent
0af14fc81a
commit
5b6c98582e
|
@ -1,57 +0,0 @@
|
||||||
package api
|
|
||||||
|
|
||||||
const (
|
|
||||||
// LocalStorage means the storage service is local file system.
|
|
||||||
LocalStorage = -1
|
|
||||||
// DatabaseStorage means the storage service is database.
|
|
||||||
DatabaseStorage = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
type StorageType string
|
|
||||||
|
|
||||||
const (
|
|
||||||
StorageS3 StorageType = "S3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StorageConfig struct {
|
|
||||||
S3Config *StorageS3Config `json:"s3Config"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StorageS3Config struct {
|
|
||||||
EndPoint string `json:"endPoint"`
|
|
||||||
Path string `json:"path"`
|
|
||||||
Region string `json:"region"`
|
|
||||||
AccessKey string `json:"accessKey"`
|
|
||||||
SecretKey string `json:"secretKey"`
|
|
||||||
Bucket string `json:"bucket"`
|
|
||||||
URLPrefix string `json:"urlPrefix"`
|
|
||||||
URLSuffix string `json:"urlSuffix"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Storage struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type StorageType `json:"type"`
|
|
||||||
Config *StorageConfig `json:"config"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StorageCreate struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type StorageType `json:"type"`
|
|
||||||
Config *StorageConfig `json:"config"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StoragePatch struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Type StorageType `json:"type"`
|
|
||||||
Name *string `json:"name"`
|
|
||||||
Config *StorageConfig `json:"config"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StorageFind struct {
|
|
||||||
ID *int `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StorageDelete struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
}
|
|
|
@ -1,8 +1,260 @@
|
||||||
package v1
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/usememos/memos/store"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// LocalStorage means the storage service is local file system.
|
// LocalStorage means the storage service is local file system.
|
||||||
LocalStorage = -1
|
LocalStorage = -1
|
||||||
// DatabaseStorage means the storage service is database.
|
// DatabaseStorage means the storage service is database.
|
||||||
DatabaseStorage = 0
|
DatabaseStorage = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type StorageType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StorageS3 StorageType = "S3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t StorageType) String() string {
|
||||||
|
return string(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
type StorageConfig struct {
|
||||||
|
S3Config *StorageS3Config `json:"s3Config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StorageS3Config struct {
|
||||||
|
EndPoint string `json:"endPoint"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Region string `json:"region"`
|
||||||
|
AccessKey string `json:"accessKey"`
|
||||||
|
SecretKey string `json:"secretKey"`
|
||||||
|
Bucket string `json:"bucket"`
|
||||||
|
URLPrefix string `json:"urlPrefix"`
|
||||||
|
URLSuffix string `json:"urlSuffix"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Storage struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type StorageType `json:"type"`
|
||||||
|
Config *StorageConfig `json:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateStorageRequest struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type StorageType `json:"type"`
|
||||||
|
Config *StorageConfig `json:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateStorageRequest struct {
|
||||||
|
Type StorageType `json:"type"`
|
||||||
|
Name *string `json:"name"`
|
||||||
|
Config *StorageConfig `json:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *APIV1Service) registerStorageRoutes(g *echo.Group) {
|
||||||
|
g.POST("/storage", func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||||
|
if !ok {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||||
|
ID: &userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
|
||||||
|
}
|
||||||
|
if user == nil || user.Role != store.RoleHost {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
create := &CreateStorageRequest{}
|
||||||
|
if err := json.NewDecoder(c.Request().Body).Decode(create); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configString := ""
|
||||||
|
if create.Type == StorageS3 && create.Config.S3Config != nil {
|
||||||
|
configBytes, err := json.Marshal(create.Config.S3Config)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
|
||||||
|
}
|
||||||
|
configString = string(configBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
storage, err := s.Store.CreateStorage(ctx, &store.Storage{
|
||||||
|
Name: create.Name,
|
||||||
|
Type: create.Type.String(),
|
||||||
|
Config: configString,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create storage").SetInternal(err)
|
||||||
|
}
|
||||||
|
storageMessage, err := ConvertStorageFromStore(storage)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err)
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusOK, storageMessage)
|
||||||
|
})
|
||||||
|
|
||||||
|
g.PATCH("/storage/:storageId", func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||||
|
if !ok {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||||
|
ID: &userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
|
||||||
|
}
|
||||||
|
if user == nil || user.Role != store.RoleHost {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
storageID, err := strconv.Atoi(c.Param("storageId"))
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("storageId"))).SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
update := &UpdateStorageRequest{}
|
||||||
|
if err := json.NewDecoder(c.Request().Body).Decode(update); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch storage request").SetInternal(err)
|
||||||
|
}
|
||||||
|
storageUpdate := &store.UpdateStorage{
|
||||||
|
ID: storageID,
|
||||||
|
}
|
||||||
|
if update.Name != nil {
|
||||||
|
storageUpdate.Name = update.Name
|
||||||
|
}
|
||||||
|
if update.Config != nil {
|
||||||
|
if update.Type == StorageS3 {
|
||||||
|
configBytes, err := json.Marshal(update.Config.S3Config)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
|
||||||
|
}
|
||||||
|
configString := string(configBytes)
|
||||||
|
storageUpdate.Config = &configString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
storage, err := s.Store.UpdateStorage(ctx, storageUpdate)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch storage").SetInternal(err)
|
||||||
|
}
|
||||||
|
storageMessage, err := ConvertStorageFromStore(storage)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err)
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusOK, storageMessage)
|
||||||
|
})
|
||||||
|
|
||||||
|
g.GET("/storage", func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||||
|
if !ok {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||||
|
ID: &userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
|
||||||
|
}
|
||||||
|
// We should only show storage list to host user.
|
||||||
|
if user == nil || user.Role != store.RoleHost {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := s.Store.ListStorages(ctx, &store.FindStorage{})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage list").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
storageList := []*Storage{}
|
||||||
|
for _, storage := range list {
|
||||||
|
storageMessage, err := ConvertStorageFromStore(storage)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err)
|
||||||
|
}
|
||||||
|
storageList = append(storageList, storageMessage)
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusOK, storageList)
|
||||||
|
})
|
||||||
|
|
||||||
|
g.DELETE("/storage/:storageId", func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||||
|
if !ok {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||||
|
ID: &userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
|
||||||
|
}
|
||||||
|
if user == nil || user.Role != store.RoleHost {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
storageID, err := strconv.Atoi(c.Param("storageId"))
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("storageId"))).SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
systemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{Name: SystemSettingStorageServiceIDName.String()})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
|
||||||
|
}
|
||||||
|
if systemSetting != nil {
|
||||||
|
storageServiceID := DatabaseStorage
|
||||||
|
err = json.Unmarshal([]byte(systemSetting.Value), &storageServiceID)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal storage service id").SetInternal(err)
|
||||||
|
}
|
||||||
|
if storageServiceID == storageID {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Storage service %d is using", storageID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = s.Store.DeleteStorage(ctx, &store.DeleteStorage{ID: storageID}); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete storage").SetInternal(err)
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusOK, true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConvertStorageFromStore(storage *store.Storage) (*Storage, error) {
|
||||||
|
storageMessage := &Storage{
|
||||||
|
ID: storage.ID,
|
||||||
|
Name: storage.Name,
|
||||||
|
Type: StorageType(storage.Type),
|
||||||
|
Config: &StorageConfig{},
|
||||||
|
}
|
||||||
|
if storageMessage.Type == StorageS3 {
|
||||||
|
s3Config := &StorageS3Config{}
|
||||||
|
if err := json.Unmarshal([]byte(storage.Config), s3Config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
storageMessage.Config = &StorageConfig{
|
||||||
|
S3Config: s3Config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return storageMessage, nil
|
||||||
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ func (s *APIV1Service) registerTagRoutes(g *echo.Group) {
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Tag name shouldn't be empty")
|
return echo.NewHTTPError(http.StatusBadRequest, "Tag name shouldn't be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
tag, err := s.Store.UpsertTagV1(ctx, &store.Tag{
|
tag, err := s.Store.UpsertTag(ctx, &store.Tag{
|
||||||
Name: tagUpsert.Name,
|
Name: tagUpsert.Name,
|
||||||
CreatorID: userID,
|
CreatorID: userID,
|
||||||
})
|
})
|
||||||
|
|
|
@ -33,4 +33,5 @@ func (s *APIV1Service) Register(rootGroup *echo.Group) {
|
||||||
s.registerUserSettingRoutes(apiV1Group)
|
s.registerUserSettingRoutes(apiV1Group)
|
||||||
s.registerTagRoutes(apiV1Group)
|
s.registerTagRoutes(apiV1Group)
|
||||||
s.registerShortcutRoutes(apiV1Group)
|
s.registerShortcutRoutes(apiV1Group)
|
||||||
|
s.registerStorageRoutes(apiV1Group)
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,7 +155,7 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
|
||||||
}
|
}
|
||||||
storageServiceID := api.DatabaseStorage
|
storageServiceID := apiv1.DatabaseStorage
|
||||||
if systemSettingStorageServiceID != nil {
|
if systemSettingStorageServiceID != nil {
|
||||||
err = json.Unmarshal([]byte(systemSettingStorageServiceID.Value), &storageServiceID)
|
err = json.Unmarshal([]byte(systemSettingStorageServiceID.Value), &storageServiceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -164,7 +164,7 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||||
}
|
}
|
||||||
|
|
||||||
publicID := common.GenUUID()
|
publicID := common.GenUUID()
|
||||||
if storageServiceID == api.DatabaseStorage {
|
if storageServiceID == apiv1.DatabaseStorage {
|
||||||
fileBytes, err := io.ReadAll(sourceFile)
|
fileBytes, err := io.ReadAll(sourceFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read file").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read file").SetInternal(err)
|
||||||
|
@ -176,7 +176,7 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||||
Size: size,
|
Size: size,
|
||||||
Blob: fileBytes,
|
Blob: fileBytes,
|
||||||
}
|
}
|
||||||
} else if storageServiceID == api.LocalStorage {
|
} else if storageServiceID == apiv1.LocalStorage {
|
||||||
// filepath.Join() should be used for local file paths,
|
// filepath.Join() should be used for local file paths,
|
||||||
// as it handles the os-specific path separator automatically.
|
// as it handles the os-specific path separator automatically.
|
||||||
// path.Join() always uses '/' as path separator.
|
// path.Join() always uses '/' as path separator.
|
||||||
|
@ -219,13 +219,17 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
|
||||||
InternalPath: filePath,
|
InternalPath: filePath,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
storage, err := s.Store.FindStorage(ctx, &api.StorageFind{ID: &storageServiceID})
|
storage, err := s.Store.GetStorage(ctx, &store.FindStorage{ID: &storageServiceID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
|
||||||
}
|
}
|
||||||
|
storageMessage, err := apiv1.ConvertStorageFromStore(storage)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
if storage.Type == api.StorageS3 {
|
if storageMessage.Type == apiv1.StorageS3 {
|
||||||
s3Config := storage.Config.S3Config
|
s3Config := storageMessage.Config.S3Config
|
||||||
s3Client, err := s3.NewClient(ctx, &s3.Config{
|
s3Client, err := s3.NewClient(ctx, &s3.Config{
|
||||||
AccessKey: s3Config.AccessKey,
|
AccessKey: s3Config.AccessKey,
|
||||||
SecretKey: s3Config.SecretKey,
|
SecretKey: s3Config.SecretKey,
|
||||||
|
|
|
@ -101,7 +101,6 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store
|
||||||
s.registerMemoRoutes(apiGroup)
|
s.registerMemoRoutes(apiGroup)
|
||||||
s.registerMemoResourceRoutes(apiGroup)
|
s.registerMemoResourceRoutes(apiGroup)
|
||||||
s.registerResourceRoutes(apiGroup)
|
s.registerResourceRoutes(apiGroup)
|
||||||
s.registerStorageRoutes(apiGroup)
|
|
||||||
s.registerMemoRelationRoutes(apiGroup)
|
s.registerMemoRelationRoutes(apiGroup)
|
||||||
|
|
||||||
apiV1Service := apiv1.NewAPIV1Service(s.Secret, profile, store)
|
apiV1Service := apiv1.NewAPIV1Service(s.Secret, profile, store)
|
||||||
|
|
|
@ -1,152 +0,0 @@
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/usememos/memos/api"
|
|
||||||
apiv1 "github.com/usememos/memos/api/v1"
|
|
||||||
"github.com/usememos/memos/common"
|
|
||||||
"github.com/usememos/memos/store"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Server) registerStorageRoutes(g *echo.Group) {
|
|
||||||
g.POST("/storage", func(c echo.Context) error {
|
|
||||||
ctx := c.Request().Context()
|
|
||||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
|
||||||
if !ok {
|
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
|
||||||
ID: &userID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
|
|
||||||
}
|
|
||||||
if user == nil || user.Role != store.RoleHost {
|
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
|
||||||
}
|
|
||||||
|
|
||||||
storageCreate := &api.StorageCreate{}
|
|
||||||
if err := json.NewDecoder(c.Request().Body).Decode(storageCreate); err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
storage, err := s.Store.CreateStorage(ctx, storageCreate)
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create storage").SetInternal(err)
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, composeResponse(storage))
|
|
||||||
})
|
|
||||||
|
|
||||||
g.PATCH("/storage/:storageId", func(c echo.Context) error {
|
|
||||||
ctx := c.Request().Context()
|
|
||||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
|
||||||
if !ok {
|
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
|
||||||
ID: &userID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
|
|
||||||
}
|
|
||||||
if user == nil || user.Role != store.RoleHost {
|
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
|
||||||
}
|
|
||||||
|
|
||||||
storageID, err := strconv.Atoi(c.Param("storageId"))
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("storageId"))).SetInternal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
storagePatch := &api.StoragePatch{
|
|
||||||
ID: storageID,
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(c.Request().Body).Decode(storagePatch); err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch storage request").SetInternal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
storage, err := s.Store.PatchStorage(ctx, storagePatch)
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch storage").SetInternal(err)
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, composeResponse(storage))
|
|
||||||
})
|
|
||||||
|
|
||||||
g.GET("/storage", func(c echo.Context) error {
|
|
||||||
ctx := c.Request().Context()
|
|
||||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
|
||||||
if !ok {
|
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
|
||||||
ID: &userID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
|
|
||||||
}
|
|
||||||
// We should only show storage list to host user.
|
|
||||||
if user == nil || user.Role != store.RoleHost {
|
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
|
||||||
}
|
|
||||||
|
|
||||||
storageList, err := s.Store.FindStorageList(ctx, &api.StorageFind{})
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage list").SetInternal(err)
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, composeResponse(storageList))
|
|
||||||
})
|
|
||||||
|
|
||||||
g.DELETE("/storage/:storageId", func(c echo.Context) error {
|
|
||||||
ctx := c.Request().Context()
|
|
||||||
userID, ok := c.Get(getUserIDContextKey()).(int)
|
|
||||||
if !ok {
|
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
|
||||||
ID: &userID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
|
|
||||||
}
|
|
||||||
if user == nil || user.Role != store.RoleHost {
|
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
|
||||||
}
|
|
||||||
|
|
||||||
storageID, err := strconv.Atoi(c.Param("storageId"))
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("storageId"))).SetInternal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
systemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{Name: apiv1.SystemSettingStorageServiceIDName.String()})
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
|
|
||||||
}
|
|
||||||
if systemSetting != nil {
|
|
||||||
storageServiceID := api.DatabaseStorage
|
|
||||||
err = json.Unmarshal([]byte(systemSetting.Value), &storageServiceID)
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal storage service id").SetInternal(err)
|
|
||||||
}
|
|
||||||
if storageServiceID == storageID {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Storage service %d is using", storageID))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = s.Store.DeleteStorage(ctx, &api.StorageDelete{ID: storageID}); err != nil {
|
|
||||||
if common.ErrorCode(err) == common.NotFound {
|
|
||||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Storage ID not found: %d", storageID))
|
|
||||||
}
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete storage").SetInternal(err)
|
|
||||||
}
|
|
||||||
return c.JSON(http.StatusOK, true)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -21,7 +21,7 @@ type ActivityMessage struct {
|
||||||
func (s *Store) CreateActivity(ctx context.Context, create *ActivityMessage) (*ActivityMessage, error) {
|
func (s *Store) CreateActivity(ctx context.Context, create *ActivityMessage) (*ActivityMessage, error) {
|
||||||
tx, err := s.db.BeginTx(ctx, nil)
|
tx, err := s.db.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
@ -39,11 +39,11 @@ func (s *Store) CreateActivity(ctx context.Context, create *ActivityMessage) (*A
|
||||||
&create.ID,
|
&create.ID,
|
||||||
&create.CreatedTs,
|
&create.CreatedTs,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
activityMessage := create
|
activityMessage := create
|
||||||
return activityMessage, nil
|
return activityMessage, nil
|
||||||
|
|
|
@ -11,11 +11,5 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r RowStatus) String() string {
|
func (r RowStatus) String() string {
|
||||||
switch r {
|
return string(r)
|
||||||
case Normal:
|
|
||||||
return "NORMAL"
|
|
||||||
case Archived:
|
|
||||||
return "ARCHIVED"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -212,7 +212,7 @@ func listShortcuts(ctx context.Context, tx *sql.Tx, find *FindShortcut) ([]*Shor
|
||||||
args...,
|
args...,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
|
@ -228,13 +228,13 @@ func listShortcuts(ctx context.Context, tx *sql.Tx, find *FindShortcut) ([]*Shor
|
||||||
&shortcut.UpdatedTs,
|
&shortcut.UpdatedTs,
|
||||||
&shortcut.RowStatus,
|
&shortcut.RowStatus,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
list = append(list, &shortcut)
|
list = append(list, &shortcut)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return list, nil
|
return list, nil
|
||||||
|
@ -253,7 +253,7 @@ func vacuumShortcut(ctx context.Context, tx *sql.Tx) error {
|
||||||
)`
|
)`
|
||||||
_, err := tx.ExecContext(ctx, stmt)
|
_, err := tx.ExecContext(ctx, stmt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return FormatError(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
332
store/storage.go
332
store/storage.go
|
@ -3,284 +3,200 @@ package store
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/usememos/memos/api"
|
|
||||||
"github.com/usememos/memos/common"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type storageRaw struct {
|
type Storage struct {
|
||||||
ID int
|
ID int
|
||||||
Name string
|
Name string
|
||||||
Type api.StorageType
|
Type string
|
||||||
Config *api.StorageConfig
|
Config string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (raw *storageRaw) toStorage() *api.Storage {
|
type FindStorage struct {
|
||||||
return &api.Storage{
|
ID *int
|
||||||
ID: raw.ID,
|
|
||||||
Name: raw.Name,
|
|
||||||
Type: raw.Type,
|
|
||||||
Config: raw.Config,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) CreateStorage(ctx context.Context, create *api.StorageCreate) (*api.Storage, error) {
|
type UpdateStorage struct {
|
||||||
|
ID int
|
||||||
|
Name *string
|
||||||
|
Config *string
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteStorage struct {
|
||||||
|
ID int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) CreateStorage(ctx context.Context, create *Storage) (*Storage, error) {
|
||||||
tx, err := s.db.BeginTx(ctx, nil)
|
tx, err := s.db.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
storageRaw, err := createStorageRaw(ctx, tx, create)
|
query := `
|
||||||
if err != nil {
|
INSERT INTO storage (
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
config
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
RETURNING id
|
||||||
|
`
|
||||||
|
if err := tx.QueryRowContext(ctx, query, create.Name, create.Type, create.Config).Scan(
|
||||||
|
&create.ID,
|
||||||
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
return nil, FormatError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return storageRaw.toStorage(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) PatchStorage(ctx context.Context, patch *api.StoragePatch) (*api.Storage, error) {
|
|
||||||
tx, err := s.db.BeginTx(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, FormatError(err)
|
|
||||||
}
|
|
||||||
defer tx.Rollback()
|
|
||||||
|
|
||||||
storageRaw, err := patchStorageRaw(ctx, tx, patch)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
storage := create
|
||||||
return nil, FormatError(err)
|
return storage, nil
|
||||||
}
|
|
||||||
|
|
||||||
return storageRaw.toStorage(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) FindStorageList(ctx context.Context, find *api.StorageFind) ([]*api.Storage, error) {
|
func (s *Store) ListStorages(ctx context.Context, find *FindStorage) ([]*Storage, error) {
|
||||||
tx, err := s.db.BeginTx(ctx, nil)
|
tx, err := s.db.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
|
||||||
return nil, FormatError(err)
|
|
||||||
}
|
|
||||||
defer tx.Rollback()
|
|
||||||
|
|
||||||
storageRawList, err := findStorageRawList(ctx, tx, find)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
list := []*api.Storage{}
|
list, err := listStorages(ctx, tx, find)
|
||||||
for _, raw := range storageRawList {
|
if err != nil {
|
||||||
list = append(list, raw.toStorage())
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return list, nil
|
return list, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) FindStorage(ctx context.Context, find *api.StorageFind) (*api.Storage, error) {
|
func (s *Store) GetStorage(ctx context.Context, find *FindStorage) (*Storage, error) {
|
||||||
tx, err := s.db.BeginTx(ctx, nil)
|
tx, err := s.db.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
|
||||||
return nil, FormatError(err)
|
|
||||||
}
|
|
||||||
defer tx.Rollback()
|
|
||||||
|
|
||||||
list, err := findStorageRawList(ctx, tx, find)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
list, err := listStorages(ctx, tx, find)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if len(list) == 0 {
|
if len(list) == 0 {
|
||||||
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
storageRaw := list[0]
|
return list[0], nil
|
||||||
return storageRaw.toStorage(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) DeleteStorage(ctx context.Context, delete *api.StorageDelete) error {
|
func (s *Store) UpdateStorage(ctx context.Context, update *UpdateStorage) (*Storage, error) {
|
||||||
tx, err := s.db.BeginTx(ctx, nil)
|
tx, err := s.db.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return FormatError(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
if err := deleteStorage(ctx, tx, delete); err != nil {
|
|
||||||
return FormatError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
|
||||||
return FormatError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createStorageRaw(ctx context.Context, tx *sql.Tx, create *api.StorageCreate) (*storageRaw, error) {
|
|
||||||
set := []string{"name", "type", "config"}
|
|
||||||
args := []any{create.Name, create.Type}
|
|
||||||
placeholder := []string{"?", "?", "?"}
|
|
||||||
|
|
||||||
var configBytes []byte
|
|
||||||
var err error
|
|
||||||
if create.Type == api.StorageS3 {
|
|
||||||
configBytes, err = json.Marshal(create.Config.S3Config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("unsupported storage type %s", string(create.Type))
|
|
||||||
}
|
|
||||||
args = append(args, string(configBytes))
|
|
||||||
|
|
||||||
query := `
|
|
||||||
INSERT INTO storage (
|
|
||||||
` + strings.Join(set, ", ") + `
|
|
||||||
)
|
|
||||||
VALUES (` + strings.Join(placeholder, ",") + `)
|
|
||||||
RETURNING id
|
|
||||||
`
|
|
||||||
storageRaw := storageRaw{
|
|
||||||
Name: create.Name,
|
|
||||||
Type: create.Type,
|
|
||||||
Config: create.Config,
|
|
||||||
}
|
|
||||||
if err := tx.QueryRowContext(ctx, query, args...).Scan(
|
|
||||||
&storageRaw.ID,
|
|
||||||
); err != nil {
|
|
||||||
return nil, FormatError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &storageRaw, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func patchStorageRaw(ctx context.Context, tx *sql.Tx, patch *api.StoragePatch) (*storageRaw, error) {
|
|
||||||
set, args := []string{}, []any{}
|
set, args := []string{}, []any{}
|
||||||
if v := patch.Name; v != nil {
|
if update.Name != nil {
|
||||||
set, args = append(set, "name = ?"), append(args, *v)
|
set = append(set, "name = ?")
|
||||||
|
args = append(args, *update.Name)
|
||||||
}
|
}
|
||||||
if v := patch.Config; v != nil {
|
if update.Config != nil {
|
||||||
var configBytes []byte
|
set = append(set, "config = ?")
|
||||||
var err error
|
args = append(args, *update.Config)
|
||||||
if patch.Type == api.StorageS3 {
|
|
||||||
configBytes, err = json.Marshal(patch.Config.S3Config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("unsupported storage type %s", string(patch.Type))
|
|
||||||
}
|
|
||||||
set, args = append(set, "config = ?"), append(args, string(configBytes))
|
|
||||||
}
|
}
|
||||||
args = append(args, patch.ID)
|
args = append(args, update.ID)
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
UPDATE storage
|
UPDATE storage
|
||||||
SET ` + strings.Join(set, ", ") + `
|
SET ` + strings.Join(set, ", ") + `
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
RETURNING id, name, type, config
|
RETURNING
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
config
|
||||||
`
|
`
|
||||||
var storageRaw storageRaw
|
storage := &Storage{}
|
||||||
var storageConfig string
|
|
||||||
if err := tx.QueryRowContext(ctx, query, args...).Scan(
|
if err := tx.QueryRowContext(ctx, query, args...).Scan(
|
||||||
&storageRaw.ID,
|
&storage.ID,
|
||||||
&storageRaw.Name,
|
&storage.Name,
|
||||||
&storageRaw.Type,
|
&storage.Type,
|
||||||
&storageConfig,
|
&storage.Config,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, err
|
||||||
}
|
|
||||||
if storageRaw.Type == api.StorageS3 {
|
|
||||||
s3Config := &api.StorageS3Config{}
|
|
||||||
if err := json.Unmarshal([]byte(storageConfig), s3Config); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
storageRaw.Config = &api.StorageConfig{
|
|
||||||
S3Config: s3Config,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("unsupported storage type %s", string(storageRaw.Type))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &storageRaw, nil
|
if err := tx.Commit(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return storage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func findStorageRawList(ctx context.Context, tx *sql.Tx, find *api.StorageFind) ([]*storageRaw, error) {
|
func (s *Store) DeleteStorage(ctx context.Context, delete *DeleteStorage) error {
|
||||||
where, args := []string{"1 = 1"}, []any{}
|
tx, err := s.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
if v := find.ID; v != nil {
|
return err
|
||||||
where, args = append(where, "id = ?"), append(args, *v)
|
|
||||||
}
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
query := `
|
query := `
|
||||||
SELECT
|
DELETE FROM storage
|
||||||
id,
|
WHERE id = ?
|
||||||
name,
|
|
||||||
type,
|
|
||||||
config
|
|
||||||
FROM storage
|
|
||||||
WHERE ` + strings.Join(where, " AND ") + `
|
|
||||||
ORDER BY id DESC
|
|
||||||
`
|
`
|
||||||
rows, err := tx.QueryContext(ctx, query, args...)
|
if _, err := tx.ExecContext(ctx, query, delete.ID); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return nil, FormatError(err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
storageRawList := make([]*storageRaw, 0)
|
|
||||||
for rows.Next() {
|
|
||||||
var storageRaw storageRaw
|
|
||||||
var storageConfig string
|
|
||||||
if err := rows.Scan(
|
|
||||||
&storageRaw.ID,
|
|
||||||
&storageRaw.Name,
|
|
||||||
&storageRaw.Type,
|
|
||||||
&storageConfig,
|
|
||||||
); err != nil {
|
|
||||||
return nil, FormatError(err)
|
|
||||||
}
|
|
||||||
if storageRaw.Type == api.StorageS3 {
|
|
||||||
s3Config := &api.StorageS3Config{}
|
|
||||||
if err := json.Unmarshal([]byte(storageConfig), s3Config); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
storageRaw.Config = &api.StorageConfig{
|
|
||||||
S3Config: s3Config,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("unsupported storage type %s", string(storageRaw.Type))
|
|
||||||
}
|
|
||||||
storageRawList = append(storageRawList, &storageRaw)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := rows.Err(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
return nil, FormatError(err)
|
// Prevent linter warning.
|
||||||
}
|
return err
|
||||||
|
|
||||||
return storageRawList, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteStorage(ctx context.Context, tx *sql.Tx, delete *api.StorageDelete) error {
|
|
||||||
where, args := []string{"id = ?"}, []any{delete.ID}
|
|
||||||
|
|
||||||
stmt := `DELETE FROM storage WHERE ` + strings.Join(where, " AND ")
|
|
||||||
result, err := tx.ExecContext(ctx, stmt, args...)
|
|
||||||
if err != nil {
|
|
||||||
return FormatError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rows, _ := result.RowsAffected()
|
|
||||||
if rows == 0 {
|
|
||||||
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("storage not found")}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func listStorages(ctx context.Context, tx *sql.Tx, find *FindStorage) ([]*Storage, error) {
|
||||||
|
where, args := []string{"1 = 1"}, []any{}
|
||||||
|
if find.ID != nil {
|
||||||
|
where, args = append(where, "id = ?"), append(args, *find.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := tx.QueryContext(ctx, `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
config
|
||||||
|
FROM storage
|
||||||
|
WHERE `+strings.Join(where, " AND ")+`
|
||||||
|
ORDER BY id DESC`,
|
||||||
|
args...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
list := []*Storage{}
|
||||||
|
for rows.Next() {
|
||||||
|
storage := &Storage{}
|
||||||
|
if err := rows.Scan(
|
||||||
|
&storage.ID,
|
||||||
|
&storage.Name,
|
||||||
|
&storage.Type,
|
||||||
|
&storage.Config,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
list = append(list, storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
18
store/tag.go
18
store/tag.go
|
@ -21,10 +21,10 @@ type DeleteTag struct {
|
||||||
CreatorID int
|
CreatorID int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) UpsertTagV1(ctx context.Context, upsert *Tag) (*Tag, error) {
|
func (s *Store) UpsertTag(ctx context.Context, upsert *Tag) (*Tag, error) {
|
||||||
tx, err := s.db.BeginTx(ctx, nil)
|
tx, err := s.db.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ func (s *Store) UpsertTagV1(ctx context.Context, upsert *Tag) (*Tag, error) {
|
||||||
func (s *Store) ListTags(ctx context.Context, find *FindTag) ([]*Tag, error) {
|
func (s *Store) ListTags(ctx context.Context, find *FindTag) ([]*Tag, error) {
|
||||||
tx, err := s.db.BeginTx(ctx, nil)
|
tx, err := s.db.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ func (s *Store) ListTags(ctx context.Context, find *FindTag) ([]*Tag, error) {
|
||||||
`
|
`
|
||||||
rows, err := tx.QueryContext(ctx, query, args...)
|
rows, err := tx.QueryContext(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
|
@ -78,14 +78,14 @@ func (s *Store) ListTags(ctx context.Context, find *FindTag) ([]*Tag, error) {
|
||||||
&tag.Name,
|
&tag.Name,
|
||||||
&tag.CreatorID,
|
&tag.CreatorID,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
list = append(list, tag)
|
list = append(list, tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
return nil, FormatError(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return list, nil
|
return list, nil
|
||||||
|
@ -94,7 +94,7 @@ func (s *Store) ListTags(ctx context.Context, find *FindTag) ([]*Tag, error) {
|
||||||
func (s *Store) DeleteTag(ctx context.Context, delete *DeleteTag) error {
|
func (s *Store) DeleteTag(ctx context.Context, delete *DeleteTag) error {
|
||||||
tx, err := s.db.BeginTx(ctx, nil)
|
tx, err := s.db.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return FormatError(err)
|
return err
|
||||||
}
|
}
|
||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
@ -102,7 +102,7 @@ func (s *Store) DeleteTag(ctx context.Context, delete *DeleteTag) error {
|
||||||
query := `DELETE FROM tag WHERE ` + strings.Join(where, " AND ")
|
query := `DELETE FROM tag WHERE ` + strings.Join(where, " AND ")
|
||||||
result, err := tx.ExecContext(ctx, query, args...)
|
result, err := tx.ExecContext(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return FormatError(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, _ := result.RowsAffected()
|
rows, _ := result.RowsAffected()
|
||||||
|
@ -131,7 +131,7 @@ func vacuumTag(ctx context.Context, tx *sql.Tx) error {
|
||||||
)`
|
)`
|
||||||
_, err := tx.ExecContext(ctx, stmt)
|
_, err := tx.ExecContext(ctx, stmt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return FormatError(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
38
test/store/storage_test.go
Normal file
38
test/store/storage_test.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package teststore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/usememos/memos/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStorageStore(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
ts := NewTestingStore(ctx, t)
|
||||||
|
storage, err := ts.CreateStorage(ctx, &store.Storage{
|
||||||
|
Name: "test_storage",
|
||||||
|
Type: "S3",
|
||||||
|
Config: "{}",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
newStorageName := "new_storage_name"
|
||||||
|
updatedStorage, err := ts.UpdateStorage(ctx, &store.UpdateStorage{
|
||||||
|
ID: storage.ID,
|
||||||
|
Name: &newStorageName,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, newStorageName, updatedStorage.Name)
|
||||||
|
storageList, err := ts.ListStorages(ctx, &store.FindStorage{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(storageList))
|
||||||
|
require.Equal(t, updatedStorage, storageList[0])
|
||||||
|
err = ts.DeleteStorage(ctx, &store.DeleteStorage{
|
||||||
|
ID: storage.ID,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
storageList, err = ts.ListStorages(ctx, &store.FindStorage{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 0, len(storageList))
|
||||||
|
}
|
|
@ -22,9 +22,7 @@ const StorageSection = () => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchStorageList = async () => {
|
const fetchStorageList = async () => {
|
||||||
const {
|
const { data: storageList } = await api.getStorageList();
|
||||||
data: { data: storageList },
|
|
||||||
} = await api.getStorageList();
|
|
||||||
setStorageList(storageList);
|
setStorageList(storageList);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -230,19 +230,19 @@ export function deleteTag(tagName: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getStorageList() {
|
export function getStorageList() {
|
||||||
return axios.get<ResponseObject<ObjectStorage[]>>(`/api/storage`);
|
return axios.get<ObjectStorage[]>(`/api/v1/storage`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createStorage(storageCreate: StorageCreate) {
|
export function createStorage(storageCreate: StorageCreate) {
|
||||||
return axios.post<ResponseObject<ObjectStorage>>(`/api/storage`, storageCreate);
|
return axios.post<ObjectStorage>(`/api/v1/storage`, storageCreate);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function patchStorage(storagePatch: StoragePatch) {
|
export function patchStorage(storagePatch: StoragePatch) {
|
||||||
return axios.patch<ResponseObject<ObjectStorage>>(`/api/storage/${storagePatch.id}`, storagePatch);
|
return axios.patch<ObjectStorage>(`/api/v1/storage/${storagePatch.id}`, storagePatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteStorage(storageId: StorageId) {
|
export function deleteStorage(storageId: StorageId) {
|
||||||
return axios.delete(`/api/storage/${storageId}`);
|
return axios.delete(`/api/v1/storage/${storageId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getIdentityProviderList() {
|
export function getIdentityProviderList() {
|
||||||
|
|
Loading…
Reference in a new issue