resource.go 14 KB

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