resource.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. package server
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "net/http"
  8. "net/url"
  9. "path"
  10. "regexp"
  11. "strconv"
  12. "strings"
  13. "time"
  14. "github.com/labstack/echo/v4"
  15. "github.com/pkg/errors"
  16. "github.com/usememos/memos/api"
  17. "github.com/usememos/memos/common"
  18. "github.com/usememos/memos/plugin/storage/s3"
  19. )
  20. const (
  21. // The max file size is 32MB.
  22. maxFileSize = 32 << 20
  23. )
  24. var fileKeyPattern = regexp.MustCompile(`\{[a-z]{1,9}\}`)
  25. func (s *Server) registerResourceRoutes(g *echo.Group) {
  26. g.POST("/resource", func(c echo.Context) error {
  27. ctx := c.Request().Context()
  28. userID, ok := c.Get(getUserIDContextKey()).(int)
  29. if !ok {
  30. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  31. }
  32. resourceCreate := &api.ResourceCreate{}
  33. if err := json.NewDecoder(c.Request().Body).Decode(resourceCreate); err != nil {
  34. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post resource request").SetInternal(err)
  35. }
  36. resourceCreate.CreatorID = userID
  37. // Only allow those external links with http prefix.
  38. if resourceCreate.ExternalLink != "" && !strings.HasPrefix(resourceCreate.ExternalLink, "http") {
  39. return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link")
  40. }
  41. if resourceCreate.Visibility == "" {
  42. userResourceVisibilitySetting, err := s.Store.FindUserSetting(ctx, &api.UserSettingFind{
  43. UserID: userID,
  44. Key: api.UserSettingResourceVisibilityKey,
  45. })
  46. if err != nil {
  47. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user setting").SetInternal(err)
  48. }
  49. if userResourceVisibilitySetting != nil {
  50. resourceVisibility := api.Private
  51. err := json.Unmarshal([]byte(userResourceVisibilitySetting.Value), &resourceVisibility)
  52. if err != nil {
  53. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal user setting value").SetInternal(err)
  54. }
  55. resourceCreate.Visibility = resourceVisibility
  56. } else {
  57. // Private is the default resource visibility.
  58. resourceCreate.Visibility = api.Private
  59. }
  60. }
  61. resource, err := s.Store.CreateResource(ctx, resourceCreate)
  62. if err != nil {
  63. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
  64. }
  65. if err := s.createResourceCreateActivity(c, resource); err != nil {
  66. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
  67. }
  68. return c.JSON(http.StatusOK, composeResponse(resource))
  69. })
  70. g.POST("/resource/blob", func(c echo.Context) error {
  71. ctx := c.Request().Context()
  72. userID, ok := c.Get(getUserIDContextKey()).(int)
  73. if !ok {
  74. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  75. }
  76. if err := c.Request().ParseMultipartForm(maxFileSize); err != nil {
  77. return echo.NewHTTPError(http.StatusBadRequest, "Upload file overload max size").SetInternal(err)
  78. }
  79. file, err := c.FormFile("file")
  80. if err != nil {
  81. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get uploading file").SetInternal(err)
  82. }
  83. if file == nil {
  84. return echo.NewHTTPError(http.StatusBadRequest, "Upload file not found").SetInternal(err)
  85. }
  86. filename := file.Filename
  87. filetype := file.Header.Get("Content-Type")
  88. size := file.Size
  89. src, err := file.Open()
  90. if err != nil {
  91. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to open file").SetInternal(err)
  92. }
  93. defer src.Close()
  94. systemSetting, err := s.Store.FindSystemSetting(ctx, &api.SystemSettingFind{Name: api.SystemSettingStorageServiceIDName})
  95. if err != nil && common.ErrorCode(err) != common.NotFound {
  96. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
  97. }
  98. storageServiceID := 0
  99. if systemSetting != nil {
  100. err = json.Unmarshal([]byte(systemSetting.Value), &storageServiceID)
  101. if err != nil {
  102. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal storage service id").SetInternal(err)
  103. }
  104. }
  105. var resourceCreate *api.ResourceCreate
  106. if storageServiceID == 0 {
  107. fileBytes, err := io.ReadAll(src)
  108. if err != nil {
  109. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read file").SetInternal(err)
  110. }
  111. resourceCreate = &api.ResourceCreate{
  112. CreatorID: userID,
  113. Filename: filename,
  114. Type: filetype,
  115. Size: size,
  116. Blob: fileBytes,
  117. }
  118. } else {
  119. storage, err := s.Store.FindStorage(ctx, &api.StorageFind{ID: &storageServiceID})
  120. if err != nil {
  121. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
  122. }
  123. if storage.Type == api.StorageS3 {
  124. s3Config := storage.Config.S3Config
  125. t := time.Now()
  126. var s3FileKey string
  127. if s3Config.Path == "" {
  128. s3FileKey = filename
  129. } else {
  130. s3FileKey = fileKeyPattern.ReplaceAllStringFunc(s3Config.Path, func(s string) string {
  131. switch s {
  132. case "{filename}":
  133. return filename
  134. case "{filetype}":
  135. return filetype
  136. case "{timestamp}":
  137. return fmt.Sprintf("%d", t.Unix())
  138. case "{year}":
  139. return fmt.Sprintf("%d", t.Year())
  140. case "{month}":
  141. return fmt.Sprintf("%02d", t.Month())
  142. case "{day}":
  143. return fmt.Sprintf("%02d", t.Day())
  144. case "{hour}":
  145. return fmt.Sprintf("%02d", t.Hour())
  146. case "{minute}":
  147. return fmt.Sprintf("%02d", t.Minute())
  148. case "{second}":
  149. return fmt.Sprintf("%02d", t.Second())
  150. }
  151. return s
  152. })
  153. if !strings.Contains(s3Config.Path, "{filename}") {
  154. s3FileKey = path.Join(s3FileKey, filename)
  155. }
  156. }
  157. s3client, err := s3.NewClient(ctx, &s3.Config{
  158. AccessKey: s3Config.AccessKey,
  159. SecretKey: s3Config.SecretKey,
  160. EndPoint: s3Config.EndPoint,
  161. Region: s3Config.Region,
  162. Bucket: s3Config.Bucket,
  163. URLPrefix: s3Config.URLPrefix,
  164. })
  165. if err != nil {
  166. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to new s3 client").SetInternal(err)
  167. }
  168. link, err := s3client.UploadFile(ctx, s3FileKey, filetype, src)
  169. if err != nil {
  170. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upload via s3 client").SetInternal(err)
  171. }
  172. resourceCreate = &api.ResourceCreate{
  173. CreatorID: userID,
  174. Filename: filename,
  175. Type: filetype,
  176. ExternalLink: link,
  177. }
  178. } else {
  179. return echo.NewHTTPError(http.StatusInternalServerError, "Unsupported storage type")
  180. }
  181. }
  182. if resourceCreate.Visibility == "" {
  183. userResourceVisibilitySetting, err := s.Store.FindUserSetting(ctx, &api.UserSettingFind{
  184. UserID: userID,
  185. Key: api.UserSettingResourceVisibilityKey,
  186. })
  187. if err != nil {
  188. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user setting").SetInternal(err)
  189. }
  190. if userResourceVisibilitySetting != nil {
  191. resourceVisibility := api.Private
  192. err := json.Unmarshal([]byte(userResourceVisibilitySetting.Value), &resourceVisibility)
  193. if err != nil {
  194. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal user setting value").SetInternal(err)
  195. }
  196. resourceCreate.Visibility = resourceVisibility
  197. } else {
  198. // Private is the default resource visibility.
  199. resourceCreate.Visibility = api.Private
  200. }
  201. }
  202. resource, err := s.Store.CreateResource(ctx, resourceCreate)
  203. if err != nil {
  204. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
  205. }
  206. if err := s.createResourceCreateActivity(c, resource); err != nil {
  207. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
  208. }
  209. return c.JSON(http.StatusOK, composeResponse(resource))
  210. })
  211. g.GET("/resource", func(c echo.Context) error {
  212. ctx := c.Request().Context()
  213. userID, ok := c.Get(getUserIDContextKey()).(int)
  214. if !ok {
  215. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  216. }
  217. resourceFind := &api.ResourceFind{
  218. CreatorID: &userID,
  219. }
  220. list, err := s.Store.FindResourceList(ctx, resourceFind)
  221. if err != nil {
  222. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
  223. }
  224. for _, resource := range list {
  225. memoResourceList, err := s.Store.FindMemoResourceList(ctx, &api.MemoResourceFind{
  226. ResourceID: &resource.ID,
  227. })
  228. if err != nil {
  229. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo resource list").SetInternal(err)
  230. }
  231. resource.LinkedMemoAmount = len(memoResourceList)
  232. }
  233. return c.JSON(http.StatusOK, composeResponse(list))
  234. })
  235. g.GET("/resource/:resourceId", func(c echo.Context) error {
  236. ctx := c.Request().Context()
  237. resourceID, err := strconv.Atoi(c.Param("resourceId"))
  238. if err != nil {
  239. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
  240. }
  241. userID, ok := c.Get(getUserIDContextKey()).(int)
  242. if !ok {
  243. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  244. }
  245. resourceFind := &api.ResourceFind{
  246. ID: &resourceID,
  247. CreatorID: &userID,
  248. GetBlob: true,
  249. }
  250. resource, err := s.Store.FindResource(ctx, resourceFind)
  251. if err != nil {
  252. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
  253. }
  254. return c.JSON(http.StatusOK, composeResponse(resource))
  255. })
  256. g.GET("/resource/:resourceId/blob", func(c echo.Context) error {
  257. ctx := c.Request().Context()
  258. resourceID, err := strconv.Atoi(c.Param("resourceId"))
  259. if err != nil {
  260. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
  261. }
  262. userID, ok := c.Get(getUserIDContextKey()).(int)
  263. if !ok {
  264. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  265. }
  266. resourceFind := &api.ResourceFind{
  267. ID: &resourceID,
  268. CreatorID: &userID,
  269. GetBlob: true,
  270. }
  271. resource, err := s.Store.FindResource(ctx, resourceFind)
  272. if err != nil {
  273. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
  274. }
  275. return c.Stream(http.StatusOK, resource.Type, bytes.NewReader(resource.Blob))
  276. })
  277. g.PATCH("/resource/:resourceId", func(c echo.Context) error {
  278. ctx := c.Request().Context()
  279. userID, ok := c.Get(getUserIDContextKey()).(int)
  280. if !ok {
  281. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  282. }
  283. resourceID, err := strconv.Atoi(c.Param("resourceId"))
  284. if err != nil {
  285. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
  286. }
  287. resourceFind := &api.ResourceFind{
  288. ID: &resourceID,
  289. }
  290. resource, err := s.Store.FindResource(ctx, resourceFind)
  291. if err != nil {
  292. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
  293. }
  294. if resource.CreatorID != userID {
  295. return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
  296. }
  297. currentTs := time.Now().Unix()
  298. resourcePatch := &api.ResourcePatch{
  299. UpdatedTs: &currentTs,
  300. }
  301. if err := json.NewDecoder(c.Request().Body).Decode(resourcePatch); err != nil {
  302. return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch resource request").SetInternal(err)
  303. }
  304. resourcePatch.ID = resourceID
  305. resource, err = s.Store.PatchResource(ctx, resourcePatch)
  306. if err != nil {
  307. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch resource").SetInternal(err)
  308. }
  309. return c.JSON(http.StatusOK, composeResponse(resource))
  310. })
  311. g.DELETE("/resource/:resourceId", func(c echo.Context) error {
  312. ctx := c.Request().Context()
  313. userID, ok := c.Get(getUserIDContextKey()).(int)
  314. if !ok {
  315. return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
  316. }
  317. resourceID, err := strconv.Atoi(c.Param("resourceId"))
  318. if err != nil {
  319. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
  320. }
  321. resource, err := s.Store.FindResource(ctx, &api.ResourceFind{
  322. ID: &resourceID,
  323. CreatorID: &userID,
  324. })
  325. if err != nil {
  326. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
  327. }
  328. if resource.CreatorID != userID {
  329. return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
  330. }
  331. resourceDelete := &api.ResourceDelete{
  332. ID: resourceID,
  333. }
  334. if err := s.Store.DeleteResource(ctx, resourceDelete); err != nil {
  335. if common.ErrorCode(err) == common.NotFound {
  336. return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource ID not found: %d", resourceID))
  337. }
  338. return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete resource").SetInternal(err)
  339. }
  340. return c.JSON(http.StatusOK, true)
  341. })
  342. }
  343. func (s *Server) registerResourcePublicRoutes(g *echo.Group) {
  344. g.GET("/r/:resourceId/:filename", func(c echo.Context) error {
  345. ctx := c.Request().Context()
  346. resourceID, err := strconv.Atoi(c.Param("resourceId"))
  347. if err != nil {
  348. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
  349. }
  350. filename, err := url.QueryUnescape(c.Param("filename"))
  351. if err != nil {
  352. return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("filename is invalid: %s", c.Param("filename"))).SetInternal(err)
  353. }
  354. resourceFind := &api.ResourceFind{
  355. ID: &resourceID,
  356. Filename: &filename,
  357. GetBlob: true,
  358. }
  359. resource, err := s.Store.FindResource(ctx, resourceFind)
  360. if err != nil {
  361. return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find resource by ID: %v", resourceID)).SetInternal(err)
  362. }
  363. c.Response().Writer.Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
  364. c.Response().Writer.Header().Set(echo.HeaderContentSecurityPolicy, "default-src 'self'")
  365. resourceType := strings.ToLower(resource.Type)
  366. if strings.HasPrefix(resourceType, "text") {
  367. resourceType = echo.MIMETextPlainCharsetUTF8
  368. } else if strings.HasPrefix(resourceType, "video") || strings.HasPrefix(resourceType, "audio") {
  369. http.ServeContent(c.Response(), c.Request(), resource.Filename, time.Unix(resource.UpdatedTs, 0), bytes.NewReader(resource.Blob))
  370. return nil
  371. }
  372. return c.Stream(http.StatusOK, resourceType, bytes.NewReader(resource.Blob))
  373. })
  374. }
  375. func (s *Server) createResourceCreateActivity(c echo.Context, resource *api.Resource) error {
  376. ctx := c.Request().Context()
  377. payload := api.ActivityResourceCreatePayload{
  378. Filename: resource.Filename,
  379. Type: resource.Type,
  380. Size: resource.Size,
  381. }
  382. payloadBytes, err := json.Marshal(payload)
  383. if err != nil {
  384. return errors.Wrap(err, "failed to marshal activity payload")
  385. }
  386. activity, err := s.Store.CreateActivity(ctx, &api.ActivityCreate{
  387. CreatorID: resource.CreatorID,
  388. Type: api.ActivityResourceCreate,
  389. Level: api.ActivityInfo,
  390. Payload: string(payloadBytes),
  391. })
  392. if err != nil || activity == nil {
  393. return errors.Wrap(err, "failed to create activity")
  394. }
  395. return err
  396. }