resource.go 12 KB

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