123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315 |
- package v1
- import (
- "encoding/json"
- "fmt"
- "net/http"
- "github.com/labstack/echo/v4"
- "github.com/usememos/memos/common/util"
- "github.com/usememos/memos/store"
- )
- const (
- // LocalStorage means the storage service is local file system.
- LocalStorage int32 = -1
- // DatabaseStorage means the storage service is database.
- DatabaseStorage int32 = 0
- // Default storage service is database.
- DefaultStorage int32 = DatabaseStorage
- )
- 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 int32 `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.GET("/storage", s.GetStorageList)
- g.POST("/storage", s.CreateStorage)
- g.PATCH("/storage/:storageId", s.UpdateStorage)
- g.DELETE("/storage/:storageId", s.DeleteStorage)
- }
- // GetStorageList godoc
- //
- // @Summary Get a list of storages
- // @Tags storage
- // @Produce json
- // @Success 200 {object} []store.Storage "List of storages"
- // @Failure 401 {object} nil "Missing user in session | Unauthorized"
- // @Failure 500 {object} nil "Failed to find user | Failed to convert storage"
- // @Router /api/v1/storage [GET]
- func (s *APIV1Service) GetStorageList(c echo.Context) error {
- ctx := c.Request().Context()
- userID, ok := c.Get(userIDContextKey).(int32)
- 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)
- }
- // CreateStorage godoc
- //
- // @Summary Create storage
- // @Tags storage
- // @Accept json
- // @Produce json
- // @Param body body CreateStorageRequest true "Request object."
- // @Success 200 {object} store.Storage "Created storage"
- // @Failure 400 {object} nil "Malformatted post storage request"
- // @Failure 401 {object} nil "Missing user in session"
- // @Failure 500 {object} nil "Failed to find user | Failed to create storage | Failed to convert storage"
- // @Router /api/v1/storage [POST]
- func (s *APIV1Service) CreateStorage(c echo.Context) error {
- ctx := c.Request().Context()
- userID, ok := c.Get(userIDContextKey).(int32)
- 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)
- }
- // DeleteStorage godoc
- //
- // @Summary Delete a storage
- // @Tags storage
- // @Produce json
- // @Param storageId path int true "Storage ID"
- // @Success 200 {boolean} true "Storage deleted"
- // @Failure 400 {object} nil "ID is not a number: %s | Storage service %d is using"
- // @Failure 401 {object} nil "Missing user in session | Unauthorized"
- // @Failure 500 {object} nil "Failed to find user | Failed to find storage | Failed to unmarshal storage service id | Failed to delete storage"
- // @Router /api/v1/storage/{storageId} [DELETE]
- //
- // NOTES:
- // - error message "Storage service %d is using" probably should be "Storage service %d is in use".
- func (s *APIV1Service) DeleteStorage(c echo.Context) error {
- ctx := c.Request().Context()
- userID, ok := c.Get(userIDContextKey).(int32)
- 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 := util.ConvertStringToInt32(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 := DefaultStorage
- 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)
- }
- // UpdateStorage godoc
- //
- // @Summary Update a storage
- // @Tags storage
- // @Produce json
- // @Param storageId path int true "Storage ID"
- // @Param patch body UpdateStorageRequest true "Patch request"
- // @Success 200 {object} store.Storage "Updated resource"
- // @Failure 400 {object} nil "ID is not a number: %s | Malformatted patch storage request | Malformatted post storage request"
- // @Failure 401 {object} nil "Missing user in session | Unauthorized"
- // @Failure 500 {object} nil "Failed to find user | Failed to patch storage | Failed to convert storage"
- // @Router /api/v1/storage/{storageId} [PATCH]
- func (s *APIV1Service) UpdateStorage(c echo.Context) error {
- ctx := c.Request().Context()
- userID, ok := c.Get(userIDContextKey).(int32)
- 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 := util.ConvertStringToInt32(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)
- }
- 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
- }
|