resource.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  1. package v1
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "mime"
  9. "net/http"
  10. "net/url"
  11. "os"
  12. "path"
  13. "path/filepath"
  14. "regexp"
  15. "strconv"
  16. "strings"
  17. "sync/atomic"
  18. "time"
  19. "github.com/disintegration/imaging"
  20. "github.com/labstack/echo/v4"
  21. "github.com/pkg/errors"
  22. "github.com/usememos/memos/api/auth"
  23. "github.com/usememos/memos/common/log"
  24. "github.com/usememos/memos/common/util"
  25. "github.com/usememos/memos/plugin/storage/s3"
  26. "github.com/usememos/memos/store"
  27. "go.uber.org/zap"
  28. )
  29. type Resource struct {
  30. ID int32 `json:"id"`
  31. // Standard fields
  32. CreatorID int32 `json:"creatorId"`
  33. CreatedTs int64 `json:"createdTs"`
  34. UpdatedTs int64 `json:"updatedTs"`
  35. // Domain specific fields
  36. Filename string `json:"filename"`
  37. Blob []byte `json:"-"`
  38. InternalPath string `json:"-"`
  39. ExternalLink string `json:"externalLink"`
  40. Type string `json:"type"`
  41. Size int64 `json:"size"`
  42. // Related fields
  43. LinkedMemoAmount int `json:"linkedMemoAmount"`
  44. }
  45. type CreateResourceRequest struct {
  46. Filename string `json:"filename"`
  47. InternalPath string `json:"internalPath"`
  48. ExternalLink string `json:"externalLink"`
  49. Type string `json:"type"`
  50. DownloadToLocal bool `json:"downloadToLocal"`
  51. }
  52. type FindResourceRequest struct {
  53. ID *int32 `json:"id"`
  54. CreatorID *int32 `json:"creatorId"`
  55. Filename *string `json:"filename"`
  56. }
  57. type UpdateResourceRequest struct {
  58. Filename *string `json:"filename"`
  59. }
  60. const (
  61. // The upload memory buffer is 32 MiB.
  62. // It should be kept low, so RAM usage doesn't get out of control.
  63. // This is unrelated to maximum upload size limit, which is now set through system setting.
  64. maxUploadBufferSizeBytes = 32 << 20
  65. MebiByte = 1024 * 1024
  66. // thumbnailImagePath is the directory to store image thumbnails.
  67. thumbnailImagePath = ".thumbnail_cache"
  68. )
  69. var fileKeyPattern = regexp.MustCompile(`\{[a-z]{1,9}\}`)
  70. func (s *APIV1Service) registerResourceRoutes(g *echo.Group) {
  71. g.POST("/resource", func(c echo.Context) error {
  72. ctx := c.Request().Context()
  73. userID, ok := c.Get(auth.UserIDContextKey).(int32)
  74. if !ok {
  75. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  76. }
  77. request := &CreateResourceRequest{}
  78. if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
  79. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post resource request").SetInternal(err)
  80. }
  81. create := &store.Resource{
  82. CreatorID: userID,
  83. Filename: request.Filename,
  84. ExternalLink: request.ExternalLink,
  85. Type: request.Type,
  86. }
  87. if request.ExternalLink != "" {
  88. // Only allow those external links scheme with http/https
  89. linkURL, err := url.Parse(request.ExternalLink)
  90. if err != nil {
  91. return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link").SetInternal(err)
  92. }
  93. if linkURL.Scheme != "http" && linkURL.Scheme != "https" {
  94. return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link scheme")
  95. }
  96. if request.DownloadToLocal {
  97. resp, err := http.Get(linkURL.String())
  98. if err != nil {
  99. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Failed to request %s", request.ExternalLink))
  100. }
  101. defer resp.Body.Close()
  102. blob, err := io.ReadAll(resp.Body)
  103. if err != nil {
  104. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Failed to read %s", request.ExternalLink))
  105. }
  106. mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
  107. if err != nil {
  108. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Failed to read mime from %s", request.ExternalLink))
  109. }
  110. create.Type = mediaType
  111. filename := path.Base(linkURL.Path)
  112. if path.Ext(filename) == "" {
  113. extensions, _ := mime.ExtensionsByType(mediaType)
  114. if len(extensions) > 0 {
  115. filename += extensions[0]
  116. }
  117. }
  118. create.Filename = filename
  119. create.ExternalLink = ""
  120. create.Size = int64(len(blob))
  121. err = SaveResourceBlob(ctx, s.Store, create, bytes.NewReader(blob))
  122. if err != nil {
  123. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to save resource").SetInternal(err)
  124. }
  125. }
  126. }
  127. resource, err := s.Store.CreateResource(ctx, create)
  128. if err != nil {
  129. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
  130. }
  131. if err := s.createResourceCreateActivity(ctx, resource); err != nil {
  132. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
  133. }
  134. return c.JSON(http.StatusOK, convertResourceFromStore(resource))
  135. })
  136. g.POST("/resource/blob", func(c echo.Context) error {
  137. ctx := c.Request().Context()
  138. userID, ok := c.Get(auth.UserIDContextKey).(int32)
  139. if !ok {
  140. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  141. }
  142. // This is the backend default max upload size limit.
  143. maxUploadSetting := s.Store.GetSystemSettingValueWithDefault(&ctx, SystemSettingMaxUploadSizeMiBName.String(), "32")
  144. var settingMaxUploadSizeBytes int
  145. if settingMaxUploadSizeMiB, err := strconv.Atoi(maxUploadSetting); err == nil {
  146. settingMaxUploadSizeBytes = settingMaxUploadSizeMiB * MebiByte
  147. } else {
  148. log.Warn("Failed to parse max upload size", zap.Error(err))
  149. settingMaxUploadSizeBytes = 0
  150. }
  151. file, err := c.FormFile("file")
  152. if err != nil {
  153. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get uploading file").SetInternal(err)
  154. }
  155. if file == nil {
  156. return echo.NewHTTPError(http.StatusBadRequest, "Upload file not found").SetInternal(err)
  157. }
  158. if file.Size > int64(settingMaxUploadSizeBytes) {
  159. message := fmt.Sprintf("File size exceeds allowed limit of %d MiB", settingMaxUploadSizeBytes/MebiByte)
  160. return echo.NewHTTPError(http.StatusBadRequest, message).SetInternal(err)
  161. }
  162. if err := c.Request().ParseMultipartForm(maxUploadBufferSizeBytes); err != nil {
  163. return echo.NewHTTPError(http.StatusBadRequest, "Failed to parse upload data").SetInternal(err)
  164. }
  165. sourceFile, err := file.Open()
  166. if err != nil {
  167. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to open file").SetInternal(err)
  168. }
  169. defer sourceFile.Close()
  170. create := &store.Resource{
  171. CreatorID: userID,
  172. Filename: file.Filename,
  173. Type: file.Header.Get("Content-Type"),
  174. Size: file.Size,
  175. }
  176. err = SaveResourceBlob(ctx, s.Store, create, sourceFile)
  177. if err != nil {
  178. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to save resource").SetInternal(err)
  179. }
  180. resource, err := s.Store.CreateResource(ctx, create)
  181. if err != nil {
  182. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
  183. }
  184. if err := s.createResourceCreateActivity(ctx, resource); err != nil {
  185. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
  186. }
  187. return c.JSON(http.StatusOK, convertResourceFromStore(resource))
  188. })
  189. g.GET("/resource", func(c echo.Context) error {
  190. ctx := c.Request().Context()
  191. userID, ok := c.Get(auth.UserIDContextKey).(int32)
  192. if !ok {
  193. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  194. }
  195. find := &store.FindResource{
  196. CreatorID: &userID,
  197. }
  198. if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
  199. find.Limit = &limit
  200. }
  201. if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
  202. find.Offset = &offset
  203. }
  204. list, err := s.Store.ListResources(ctx, find)
  205. if err != nil {
  206. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
  207. }
  208. resourceMessageList := []*Resource{}
  209. for _, resource := range list {
  210. resourceMessageList = append(resourceMessageList, convertResourceFromStore(resource))
  211. }
  212. return c.JSON(http.StatusOK, resourceMessageList)
  213. })
  214. g.PATCH("/resource/:resourceId", func(c echo.Context) error {
  215. ctx := c.Request().Context()
  216. userID, ok := c.Get(auth.UserIDContextKey).(int32)
  217. if !ok {
  218. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  219. }
  220. resourceID, err := util.ConvertStringToInt32(c.Param("resourceId"))
  221. if err != nil {
  222. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
  223. }
  224. resource, err := s.Store.GetResource(ctx, &store.FindResource{
  225. ID: &resourceID,
  226. })
  227. if err != nil {
  228. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
  229. }
  230. if resource == nil {
  231. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource not found: %d", resourceID))
  232. }
  233. if resource.CreatorID != userID {
  234. return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
  235. }
  236. request := &UpdateResourceRequest{}
  237. if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
  238. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch resource request").SetInternal(err)
  239. }
  240. currentTs := time.Now().Unix()
  241. update := &store.UpdateResource{
  242. ID: resourceID,
  243. UpdatedTs: &currentTs,
  244. }
  245. if request.Filename != nil && *request.Filename != "" {
  246. update.Filename = request.Filename
  247. }
  248. resource, err = s.Store.UpdateResource(ctx, update)
  249. if err != nil {
  250. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch resource").SetInternal(err)
  251. }
  252. return c.JSON(http.StatusOK, convertResourceFromStore(resource))
  253. })
  254. g.DELETE("/resource/:resourceId", func(c echo.Context) error {
  255. ctx := c.Request().Context()
  256. userID, ok := c.Get(auth.UserIDContextKey).(int32)
  257. if !ok {
  258. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  259. }
  260. resourceID, err := util.ConvertStringToInt32(c.Param("resourceId"))
  261. if err != nil {
  262. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
  263. }
  264. resource, err := s.Store.GetResource(ctx, &store.FindResource{
  265. ID: &resourceID,
  266. CreatorID: &userID,
  267. })
  268. if err != nil {
  269. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
  270. }
  271. if resource == nil {
  272. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource not found: %d", resourceID))
  273. }
  274. if resource.InternalPath != "" {
  275. if err := os.Remove(resource.InternalPath); err != nil {
  276. log.Warn(fmt.Sprintf("failed to delete local file with path %s", resource.InternalPath), zap.Error(err))
  277. }
  278. }
  279. ext := filepath.Ext(resource.Filename)
  280. thumbnailPath := filepath.Join(s.Profile.Data, thumbnailImagePath, fmt.Sprintf("%d%s", resource.ID, ext))
  281. if err := os.Remove(thumbnailPath); err != nil {
  282. log.Warn(fmt.Sprintf("failed to delete local thumbnail with path %s", thumbnailPath), zap.Error(err))
  283. }
  284. if err := s.Store.DeleteResource(ctx, &store.DeleteResource{
  285. ID: resourceID,
  286. }); err != nil {
  287. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete resource").SetInternal(err)
  288. }
  289. return c.JSON(http.StatusOK, true)
  290. })
  291. }
  292. func (s *APIV1Service) registerResourcePublicRoutes(g *echo.Group) {
  293. f := func(c echo.Context) error {
  294. ctx := c.Request().Context()
  295. resourceID, err := util.ConvertStringToInt32(c.Param("resourceId"))
  296. if err != nil {
  297. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
  298. }
  299. resourceVisibility, err := checkResourceVisibility(ctx, s.Store, resourceID)
  300. if err != nil {
  301. return echo.NewHTTPError(http.StatusBadRequest, "Failed to get resource visibility").SetInternal(err)
  302. }
  303. // Protected resource require a logined user
  304. userID, ok := c.Get(auth.UserIDContextKey).(int32)
  305. if resourceVisibility == store.Protected && (!ok || userID <= 0) {
  306. return echo.NewHTTPError(http.StatusUnauthorized, "Resource visibility not match").SetInternal(err)
  307. }
  308. resource, err := s.Store.GetResource(ctx, &store.FindResource{
  309. ID: &resourceID,
  310. GetBlob: true,
  311. })
  312. if err != nil {
  313. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find resource by ID: %v", resourceID)).SetInternal(err)
  314. }
  315. if resource == nil {
  316. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource not found: %d", resourceID))
  317. }
  318. // Private resource require logined user is the creator
  319. if resourceVisibility == store.Private && (!ok || userID != resource.CreatorID) {
  320. return echo.NewHTTPError(http.StatusUnauthorized, "Resource visibility not match").SetInternal(err)
  321. }
  322. blob := resource.Blob
  323. if resource.InternalPath != "" {
  324. resourcePath := resource.InternalPath
  325. src, err := os.Open(resourcePath)
  326. if err != nil {
  327. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to open the local resource: %s", resourcePath)).SetInternal(err)
  328. }
  329. defer src.Close()
  330. blob, err = io.ReadAll(src)
  331. if err != nil {
  332. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to read the local resource: %s", resourcePath)).SetInternal(err)
  333. }
  334. }
  335. if c.QueryParam("thumbnail") == "1" && util.HasPrefixes(resource.Type, "image/png", "image/jpeg") {
  336. ext := filepath.Ext(resource.Filename)
  337. thumbnailPath := filepath.Join(s.Profile.Data, thumbnailImagePath, fmt.Sprintf("%d%s", resource.ID, ext))
  338. thumbnailBlob, err := getOrGenerateThumbnailImage(blob, thumbnailPath)
  339. if err != nil {
  340. log.Warn(fmt.Sprintf("failed to get or generate local thumbnail with path %s", thumbnailPath), zap.Error(err))
  341. } else {
  342. blob = thumbnailBlob
  343. }
  344. }
  345. c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
  346. c.Response().Writer.Header().Set(echo.HeaderContentSecurityPolicy, "default-src 'self'")
  347. resourceType := strings.ToLower(resource.Type)
  348. if strings.HasPrefix(resourceType, "text") {
  349. resourceType = echo.MIMETextPlainCharsetUTF8
  350. } else if strings.HasPrefix(resourceType, "video") || strings.HasPrefix(resourceType, "audio") {
  351. http.ServeContent(c.Response(), c.Request(), resource.Filename, time.Unix(resource.UpdatedTs, 0), bytes.NewReader(blob))
  352. return nil
  353. }
  354. return c.Stream(http.StatusOK, resourceType, bytes.NewReader(blob))
  355. }
  356. g.GET("/r/:resourceId", f)
  357. g.GET("/r/:resourceId/*", f)
  358. }
  359. func (s *APIV1Service) createResourceCreateActivity(ctx context.Context, resource *store.Resource) error {
  360. payload := ActivityResourceCreatePayload{
  361. Filename: resource.Filename,
  362. Type: resource.Type,
  363. Size: resource.Size,
  364. }
  365. payloadBytes, err := json.Marshal(payload)
  366. if err != nil {
  367. return errors.Wrap(err, "failed to marshal activity payload")
  368. }
  369. activity, err := s.Store.CreateActivity(ctx, &store.Activity{
  370. CreatorID: resource.CreatorID,
  371. Type: ActivityResourceCreate.String(),
  372. Level: ActivityInfo.String(),
  373. Payload: string(payloadBytes),
  374. })
  375. if err != nil || activity == nil {
  376. return errors.Wrap(err, "failed to create activity")
  377. }
  378. return err
  379. }
  380. func replacePathTemplate(path, filename string) string {
  381. t := time.Now()
  382. path = fileKeyPattern.ReplaceAllStringFunc(path, func(s string) string {
  383. switch s {
  384. case "{filename}":
  385. return filename
  386. case "{timestamp}":
  387. return fmt.Sprintf("%d", t.Unix())
  388. case "{year}":
  389. return fmt.Sprintf("%d", t.Year())
  390. case "{month}":
  391. return fmt.Sprintf("%02d", t.Month())
  392. case "{day}":
  393. return fmt.Sprintf("%02d", t.Day())
  394. case "{hour}":
  395. return fmt.Sprintf("%02d", t.Hour())
  396. case "{minute}":
  397. return fmt.Sprintf("%02d", t.Minute())
  398. case "{second}":
  399. return fmt.Sprintf("%02d", t.Second())
  400. }
  401. return s
  402. })
  403. return path
  404. }
  405. var availableGeneratorAmount int32 = 32
  406. func getOrGenerateThumbnailImage(srcBlob []byte, dstPath string) ([]byte, error) {
  407. if _, err := os.Stat(dstPath); err != nil {
  408. if !errors.Is(err, os.ErrNotExist) {
  409. return nil, errors.Wrap(err, "failed to check thumbnail image stat")
  410. }
  411. if atomic.LoadInt32(&availableGeneratorAmount) <= 0 {
  412. return nil, errors.New("not enough available generator amount")
  413. }
  414. atomic.AddInt32(&availableGeneratorAmount, -1)
  415. defer func() {
  416. atomic.AddInt32(&availableGeneratorAmount, 1)
  417. }()
  418. reader := bytes.NewReader(srcBlob)
  419. src, err := imaging.Decode(reader, imaging.AutoOrientation(true))
  420. if err != nil {
  421. return nil, errors.Wrap(err, "failed to decode thumbnail image")
  422. }
  423. thumbnailImage := imaging.Resize(src, 512, 0, imaging.Lanczos)
  424. dstDir := path.Dir(dstPath)
  425. if err := os.MkdirAll(dstDir, os.ModePerm); err != nil {
  426. return nil, errors.Wrap(err, "failed to create thumbnail dir")
  427. }
  428. if err := imaging.Save(thumbnailImage, dstPath); err != nil {
  429. return nil, errors.Wrap(err, "failed to resize thumbnail image")
  430. }
  431. }
  432. dstFile, err := os.Open(dstPath)
  433. if err != nil {
  434. return nil, errors.Wrap(err, "failed to open the local resource")
  435. }
  436. defer dstFile.Close()
  437. dstBlob, err := io.ReadAll(dstFile)
  438. if err != nil {
  439. return nil, errors.Wrap(err, "failed to read the local resource")
  440. }
  441. return dstBlob, nil
  442. }
  443. func checkResourceVisibility(ctx context.Context, s *store.Store, resourceID int32) (store.Visibility, error) {
  444. memoResources, err := s.ListMemoResources(ctx, &store.FindMemoResource{
  445. ResourceID: &resourceID,
  446. })
  447. if err != nil {
  448. return store.Private, err
  449. }
  450. // If resource is belongs to no memo, it'll always PRIVATE.
  451. if len(memoResources) == 0 {
  452. return store.Private, nil
  453. }
  454. memoIDs := make([]int32, 0, len(memoResources))
  455. for _, memoResource := range memoResources {
  456. memoIDs = append(memoIDs, memoResource.MemoID)
  457. }
  458. visibilityList, err := s.FindMemosVisibilityList(ctx, memoIDs)
  459. if err != nil {
  460. return store.Private, err
  461. }
  462. var isProtected bool
  463. for _, visibility := range visibilityList {
  464. // If any memo is PUBLIC, resource should be PUBLIC too.
  465. if visibility == store.Public {
  466. return store.Public, nil
  467. }
  468. if visibility == store.Protected {
  469. isProtected = true
  470. }
  471. }
  472. if isProtected {
  473. return store.Protected, nil
  474. }
  475. return store.Private, nil
  476. }
  477. func convertResourceFromStore(resource *store.Resource) *Resource {
  478. return &Resource{
  479. ID: resource.ID,
  480. CreatorID: resource.CreatorID,
  481. CreatedTs: resource.CreatedTs,
  482. UpdatedTs: resource.UpdatedTs,
  483. Filename: resource.Filename,
  484. Blob: resource.Blob,
  485. InternalPath: resource.InternalPath,
  486. ExternalLink: resource.ExternalLink,
  487. Type: resource.Type,
  488. Size: resource.Size,
  489. LinkedMemoAmount: resource.LinkedMemoAmount,
  490. }
  491. }
  492. // SaveResourceBlob save the blob of resource based on the storage config
  493. //
  494. // Depend on the storage config, some fields of *store.ResourceCreate will be changed:
  495. // 1. *DatabaseStorage*: `create.Blob`.
  496. // 2. *LocalStorage*: `create.InternalPath`.
  497. // 3. Others( external service): `create.ExternalLink`.
  498. func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resource, r io.Reader) error {
  499. systemSettingStorageServiceID, err := s.GetSystemSetting(ctx, &store.FindSystemSetting{Name: SystemSettingStorageServiceIDName.String()})
  500. if err != nil {
  501. return fmt.Errorf("Failed to find SystemSettingStorageServiceIDName: %s", err)
  502. }
  503. storageServiceID := DatabaseStorage
  504. if systemSettingStorageServiceID != nil {
  505. err = json.Unmarshal([]byte(systemSettingStorageServiceID.Value), &storageServiceID)
  506. if err != nil {
  507. return fmt.Errorf("Failed to unmarshal storage service id: %s", err)
  508. }
  509. }
  510. // `DatabaseStorage` means store blob into database
  511. if storageServiceID == DatabaseStorage {
  512. fileBytes, err := io.ReadAll(r)
  513. if err != nil {
  514. return fmt.Errorf("Failed to read file: %s", err)
  515. }
  516. create.Blob = fileBytes
  517. return nil
  518. }
  519. // `LocalStorage` means save blob into local disk
  520. if storageServiceID == LocalStorage {
  521. systemSettingLocalStoragePath, err := s.GetSystemSetting(ctx, &store.FindSystemSetting{Name: SystemSettingLocalStoragePathName.String()})
  522. if err != nil {
  523. return fmt.Errorf("Failed to find SystemSettingLocalStoragePathName: %s", err)
  524. }
  525. localStoragePath := "assets/{filename}"
  526. if systemSettingLocalStoragePath != nil && systemSettingLocalStoragePath.Value != "" {
  527. err = json.Unmarshal([]byte(systemSettingLocalStoragePath.Value), &localStoragePath)
  528. if err != nil {
  529. return fmt.Errorf("Failed to unmarshal SystemSettingLocalStoragePathName: %s", err)
  530. }
  531. }
  532. filePath := filepath.FromSlash(localStoragePath)
  533. if !strings.Contains(filePath, "{filename}") {
  534. filePath = filepath.Join(filePath, "{filename}")
  535. }
  536. filePath = filepath.Join(s.Profile.Data, replacePathTemplate(filePath, create.Filename))
  537. dir := filepath.Dir(filePath)
  538. if err = os.MkdirAll(dir, os.ModePerm); err != nil {
  539. return fmt.Errorf("Failed to create directory: %s", err)
  540. }
  541. dst, err := os.Create(filePath)
  542. if err != nil {
  543. return fmt.Errorf("Failed to create file: %s", err)
  544. }
  545. defer dst.Close()
  546. _, err = io.Copy(dst, r)
  547. if err != nil {
  548. return fmt.Errorf("Failed to copy file: %s", err)
  549. }
  550. create.InternalPath = filePath
  551. return nil
  552. }
  553. // Others: store blob into external service, such as S3
  554. storage, err := s.GetStorage(ctx, &store.FindStorage{ID: &storageServiceID})
  555. if err != nil {
  556. return fmt.Errorf("Failed to find StorageServiceID: %s", err)
  557. }
  558. if storage == nil {
  559. return fmt.Errorf("Storage %d not found", storageServiceID)
  560. }
  561. storageMessage, err := ConvertStorageFromStore(storage)
  562. if err != nil {
  563. return fmt.Errorf("Failed to ConvertStorageFromStore: %s", err)
  564. }
  565. if storageMessage.Type != StorageS3 {
  566. return fmt.Errorf("Unsupported storage type: %s", storageMessage.Type)
  567. }
  568. s3Config := storageMessage.Config.S3Config
  569. s3Client, err := s3.NewClient(ctx, &s3.Config{
  570. AccessKey: s3Config.AccessKey,
  571. SecretKey: s3Config.SecretKey,
  572. EndPoint: s3Config.EndPoint,
  573. Region: s3Config.Region,
  574. Bucket: s3Config.Bucket,
  575. URLPrefix: s3Config.URLPrefix,
  576. URLSuffix: s3Config.URLSuffix,
  577. })
  578. if err != nil {
  579. return fmt.Errorf("Failed to create s3 client: %s", err)
  580. }
  581. filePath := s3Config.Path
  582. if !strings.Contains(filePath, "{filename}") {
  583. filePath = filepath.Join(filePath, "{filename}")
  584. }
  585. filePath = replacePathTemplate(filePath, create.Filename)
  586. link, err := s3Client.UploadFile(ctx, filePath, create.Type, r)
  587. if err != nil {
  588. return fmt.Errorf("Failed to upload via s3 client: %s", err)
  589. }
  590. create.ExternalLink = link
  591. return nil
  592. }