storage.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. package v1
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net/http"
  6. "github.com/labstack/echo/v4"
  7. "github.com/usememos/memos/common/util"
  8. "github.com/usememos/memos/store"
  9. )
  10. const (
  11. // LocalStorage means the storage service is local file system.
  12. LocalStorage int32 = -1
  13. // DatabaseStorage means the storage service is database.
  14. DatabaseStorage int32 = 0
  15. // Default storage service is database.
  16. DefaultStorage int32 = DatabaseStorage
  17. )
  18. type StorageType string
  19. const (
  20. StorageS3 StorageType = "S3"
  21. )
  22. func (t StorageType) String() string {
  23. return string(t)
  24. }
  25. type StorageConfig struct {
  26. S3Config *StorageS3Config `json:"s3Config"`
  27. }
  28. type StorageS3Config struct {
  29. EndPoint string `json:"endPoint"`
  30. Path string `json:"path"`
  31. Region string `json:"region"`
  32. AccessKey string `json:"accessKey"`
  33. SecretKey string `json:"secretKey"`
  34. Bucket string `json:"bucket"`
  35. URLPrefix string `json:"urlPrefix"`
  36. URLSuffix string `json:"urlSuffix"`
  37. }
  38. type Storage struct {
  39. ID int32 `json:"id"`
  40. Name string `json:"name"`
  41. Type StorageType `json:"type"`
  42. Config *StorageConfig `json:"config"`
  43. }
  44. type CreateStorageRequest struct {
  45. Name string `json:"name"`
  46. Type StorageType `json:"type"`
  47. Config *StorageConfig `json:"config"`
  48. }
  49. type UpdateStorageRequest struct {
  50. Type StorageType `json:"type"`
  51. Name *string `json:"name"`
  52. Config *StorageConfig `json:"config"`
  53. }
  54. func (s *APIV1Service) registerStorageRoutes(g *echo.Group) {
  55. g.GET("/storage", s.GetStorageList)
  56. g.POST("/storage", s.CreateStorage)
  57. g.PATCH("/storage/:storageId", s.UpdateStorage)
  58. g.DELETE("/storage/:storageId", s.DeleteStorage)
  59. }
  60. // GetStorageList godoc
  61. //
  62. // @Summary Get a list of storages
  63. // @Tags storage
  64. // @Produce json
  65. // @Success 200 {object} []store.Storage "List of storages"
  66. // @Failure 401 {object} nil "Missing user in session | Unauthorized"
  67. // @Failure 500 {object} nil "Failed to find user | Failed to convert storage"
  68. // @Router /api/v1/storage [GET]
  69. func (s *APIV1Service) GetStorageList(c echo.Context) error {
  70. ctx := c.Request().Context()
  71. userID, ok := c.Get(userIDContextKey).(int32)
  72. if !ok {
  73. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  74. }
  75. user, err := s.Store.GetUser(ctx, &store.FindUser{
  76. ID: &userID,
  77. })
  78. if err != nil {
  79. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
  80. }
  81. // We should only show storage list to host user.
  82. if user == nil || user.Role != store.RoleHost {
  83. return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
  84. }
  85. list, err := s.Store.ListStorages(ctx, &store.FindStorage{})
  86. if err != nil {
  87. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage list").SetInternal(err)
  88. }
  89. storageList := []*Storage{}
  90. for _, storage := range list {
  91. storageMessage, err := ConvertStorageFromStore(storage)
  92. if err != nil {
  93. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err)
  94. }
  95. storageList = append(storageList, storageMessage)
  96. }
  97. return c.JSON(http.StatusOK, storageList)
  98. }
  99. // CreateStorage godoc
  100. //
  101. // @Summary Create storage
  102. // @Tags storage
  103. // @Accept json
  104. // @Produce json
  105. // @Param body body CreateStorageRequest true "Request object."
  106. // @Success 200 {object} store.Storage "Created storage"
  107. // @Failure 400 {object} nil "Malformatted post storage request"
  108. // @Failure 401 {object} nil "Missing user in session"
  109. // @Failure 500 {object} nil "Failed to find user | Failed to create storage | Failed to convert storage"
  110. // @Router /api/v1/storage [POST]
  111. func (s *APIV1Service) CreateStorage(c echo.Context) error {
  112. ctx := c.Request().Context()
  113. userID, ok := c.Get(userIDContextKey).(int32)
  114. if !ok {
  115. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  116. }
  117. user, err := s.Store.GetUser(ctx, &store.FindUser{
  118. ID: &userID,
  119. })
  120. if err != nil {
  121. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
  122. }
  123. if user == nil || user.Role != store.RoleHost {
  124. return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
  125. }
  126. create := &CreateStorageRequest{}
  127. if err := json.NewDecoder(c.Request().Body).Decode(create); err != nil {
  128. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
  129. }
  130. configString := ""
  131. if create.Type == StorageS3 && create.Config.S3Config != nil {
  132. configBytes, err := json.Marshal(create.Config.S3Config)
  133. if err != nil {
  134. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
  135. }
  136. configString = string(configBytes)
  137. }
  138. storage, err := s.Store.CreateStorage(ctx, &store.Storage{
  139. Name: create.Name,
  140. Type: create.Type.String(),
  141. Config: configString,
  142. })
  143. if err != nil {
  144. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create storage").SetInternal(err)
  145. }
  146. storageMessage, err := ConvertStorageFromStore(storage)
  147. if err != nil {
  148. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err)
  149. }
  150. return c.JSON(http.StatusOK, storageMessage)
  151. }
  152. // DeleteStorage godoc
  153. //
  154. // @Summary Delete a storage
  155. // @Tags storage
  156. // @Produce json
  157. // @Param storageId path int true "Storage ID"
  158. // @Success 200 {boolean} true "Storage deleted"
  159. // @Failure 400 {object} nil "ID is not a number: %s | Storage service %d is using"
  160. // @Failure 401 {object} nil "Missing user in session | Unauthorized"
  161. // @Failure 500 {object} nil "Failed to find user | Failed to find storage | Failed to unmarshal storage service id | Failed to delete storage"
  162. // @Router /api/v1/storage/{storageId} [DELETE]
  163. //
  164. // NOTES:
  165. // - error message "Storage service %d is using" probably should be "Storage service %d is in use".
  166. func (s *APIV1Service) DeleteStorage(c echo.Context) error {
  167. ctx := c.Request().Context()
  168. userID, ok := c.Get(userIDContextKey).(int32)
  169. if !ok {
  170. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  171. }
  172. user, err := s.Store.GetUser(ctx, &store.FindUser{
  173. ID: &userID,
  174. })
  175. if err != nil {
  176. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
  177. }
  178. if user == nil || user.Role != store.RoleHost {
  179. return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
  180. }
  181. storageID, err := util.ConvertStringToInt32(c.Param("storageId"))
  182. if err != nil {
  183. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("storageId"))).SetInternal(err)
  184. }
  185. systemSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{Name: SystemSettingStorageServiceIDName.String()})
  186. if err != nil {
  187. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
  188. }
  189. if systemSetting != nil {
  190. storageServiceID := DefaultStorage
  191. err = json.Unmarshal([]byte(systemSetting.Value), &storageServiceID)
  192. if err != nil {
  193. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal storage service id").SetInternal(err)
  194. }
  195. if storageServiceID == storageID {
  196. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Storage service %d is using", storageID))
  197. }
  198. }
  199. if err = s.Store.DeleteStorage(ctx, &store.DeleteStorage{ID: storageID}); err != nil {
  200. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete storage").SetInternal(err)
  201. }
  202. return c.JSON(http.StatusOK, true)
  203. }
  204. // UpdateStorage godoc
  205. //
  206. // @Summary Update a storage
  207. // @Tags storage
  208. // @Produce json
  209. // @Param storageId path int true "Storage ID"
  210. // @Param patch body UpdateStorageRequest true "Patch request"
  211. // @Success 200 {object} store.Storage "Updated resource"
  212. // @Failure 400 {object} nil "ID is not a number: %s | Malformatted patch storage request | Malformatted post storage request"
  213. // @Failure 401 {object} nil "Missing user in session | Unauthorized"
  214. // @Failure 500 {object} nil "Failed to find user | Failed to patch storage | Failed to convert storage"
  215. // @Router /api/v1/storage/{storageId} [PATCH]
  216. func (s *APIV1Service) UpdateStorage(c echo.Context) error {
  217. ctx := c.Request().Context()
  218. userID, ok := c.Get(userIDContextKey).(int32)
  219. if !ok {
  220. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  221. }
  222. user, err := s.Store.GetUser(ctx, &store.FindUser{
  223. ID: &userID,
  224. })
  225. if err != nil {
  226. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
  227. }
  228. if user == nil || user.Role != store.RoleHost {
  229. return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
  230. }
  231. storageID, err := util.ConvertStringToInt32(c.Param("storageId"))
  232. if err != nil {
  233. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("storageId"))).SetInternal(err)
  234. }
  235. update := &UpdateStorageRequest{}
  236. if err := json.NewDecoder(c.Request().Body).Decode(update); err != nil {
  237. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch storage request").SetInternal(err)
  238. }
  239. storageUpdate := &store.UpdateStorage{
  240. ID: storageID,
  241. }
  242. if update.Name != nil {
  243. storageUpdate.Name = update.Name
  244. }
  245. if update.Config != nil {
  246. if update.Type == StorageS3 {
  247. configBytes, err := json.Marshal(update.Config.S3Config)
  248. if err != nil {
  249. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
  250. }
  251. configString := string(configBytes)
  252. storageUpdate.Config = &configString
  253. }
  254. }
  255. storage, err := s.Store.UpdateStorage(ctx, storageUpdate)
  256. if err != nil {
  257. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch storage").SetInternal(err)
  258. }
  259. storageMessage, err := ConvertStorageFromStore(storage)
  260. if err != nil {
  261. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err)
  262. }
  263. return c.JSON(http.StatusOK, storageMessage)
  264. }
  265. func ConvertStorageFromStore(storage *store.Storage) (*Storage, error) {
  266. storageMessage := &Storage{
  267. ID: storage.ID,
  268. Name: storage.Name,
  269. Type: StorageType(storage.Type),
  270. Config: &StorageConfig{},
  271. }
  272. if storageMessage.Type == StorageS3 {
  273. s3Config := &StorageS3Config{}
  274. if err := json.Unmarshal([]byte(storage.Config), s3Config); err != nil {
  275. return nil, err
  276. }
  277. storageMessage.Config = &StorageConfig{
  278. S3Config: s3Config,
  279. }
  280. }
  281. return storageMessage, nil
  282. }