refactor: migrate storage to apiv1 (#1890)

* refactor: migrate storage to apiv1

* chore: update

* chore: update

* chore: update
This commit is contained in:
boojack 2023-07-04 10:05:57 +08:00 committed by GitHub
parent 0af14fc81a
commit 5b6c98582e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 448 additions and 455 deletions

View file

@ -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"`
}

View file

@ -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
}

View file

@ -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,
}) })

View file

@ -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)
} }

View file

@ -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,

View file

@ -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)

View file

@ -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)
})
}

View file

@ -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

View file

@ -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 ""
} }

View file

@ -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

View file

@ -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
}

View file

@ -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

View 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))
}

View file

@ -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);
}; };

View file

@ -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() {