resource_service.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. package v1
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/binary"
  6. "fmt"
  7. "io"
  8. "os"
  9. "path/filepath"
  10. "regexp"
  11. "strings"
  12. "time"
  13. "github.com/google/cel-go/cel"
  14. "github.com/lithammer/shortuuid/v4"
  15. "github.com/pkg/errors"
  16. expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
  17. "google.golang.org/genproto/googleapis/api/httpbody"
  18. "google.golang.org/grpc/codes"
  19. "google.golang.org/grpc/status"
  20. "google.golang.org/protobuf/types/known/emptypb"
  21. "google.golang.org/protobuf/types/known/timestamppb"
  22. "github.com/usememos/memos/internal/util"
  23. "github.com/usememos/memos/plugin/storage/s3"
  24. v1pb "github.com/usememos/memos/proto/gen/api/v1"
  25. storepb "github.com/usememos/memos/proto/gen/store"
  26. "github.com/usememos/memos/store"
  27. )
  28. const (
  29. // The upload memory buffer is 32 MiB.
  30. // It should be kept low, so RAM usage doesn't get out of control.
  31. // This is unrelated to maximum upload size limit, which is now set through system setting.
  32. MaxUploadBufferSizeBytes = 32 << 20
  33. MebiByte = 1024 * 1024
  34. )
  35. func (s *APIV1Service) CreateResource(ctx context.Context, request *v1pb.CreateResourceRequest) (*v1pb.Resource, error) {
  36. user, err := s.GetCurrentUser(ctx)
  37. if err != nil {
  38. return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
  39. }
  40. create := &store.Resource{
  41. UID: shortuuid.New(),
  42. CreatorID: user.ID,
  43. Filename: request.Resource.Filename,
  44. Type: request.Resource.Type,
  45. }
  46. workspaceStorageSetting, err := s.Store.GetWorkspaceStorageSetting(ctx)
  47. if err != nil {
  48. return nil, status.Errorf(codes.Internal, "failed to get workspace storage setting: %v", err)
  49. }
  50. size := binary.Size(request.Resource.Content)
  51. uploadSizeLimit := int(workspaceStorageSetting.UploadSizeLimitMb) * MebiByte
  52. if uploadSizeLimit == 0 {
  53. uploadSizeLimit = MaxUploadBufferSizeBytes
  54. }
  55. if size > uploadSizeLimit {
  56. return nil, status.Errorf(codes.InvalidArgument, "file size exceeds the limit")
  57. }
  58. create.Size = int64(size)
  59. create.Blob = request.Resource.Content
  60. if err := SaveResourceBlob(ctx, s.Store, create); err != nil {
  61. return nil, status.Errorf(codes.Internal, "failed to save resource blob: %v", err)
  62. }
  63. if request.Resource.Memo != nil {
  64. memoID, err := ExtractMemoIDFromName(*request.Resource.Memo)
  65. if err != nil {
  66. return nil, status.Errorf(codes.InvalidArgument, "invalid memo id: %v", err)
  67. }
  68. create.MemoID = &memoID
  69. }
  70. resource, err := s.Store.CreateResource(ctx, create)
  71. if err != nil {
  72. return nil, status.Errorf(codes.Internal, "failed to create resource: %v", err)
  73. }
  74. return s.convertResourceFromStore(ctx, resource), nil
  75. }
  76. func (s *APIV1Service) ListResources(ctx context.Context, _ *v1pb.ListResourcesRequest) (*v1pb.ListResourcesResponse, error) {
  77. user, err := s.GetCurrentUser(ctx)
  78. if err != nil {
  79. return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
  80. }
  81. resources, err := s.Store.ListResources(ctx, &store.FindResource{
  82. CreatorID: &user.ID,
  83. })
  84. if err != nil {
  85. return nil, status.Errorf(codes.Internal, "failed to list resources: %v", err)
  86. }
  87. response := &v1pb.ListResourcesResponse{}
  88. for _, resource := range resources {
  89. response.Resources = append(response.Resources, s.convertResourceFromStore(ctx, resource))
  90. }
  91. return response, nil
  92. }
  93. func (s *APIV1Service) SearchResources(ctx context.Context, request *v1pb.SearchResourcesRequest) (*v1pb.SearchResourcesResponse, error) {
  94. if request.Filter == "" {
  95. return nil, status.Errorf(codes.InvalidArgument, "filter is empty")
  96. }
  97. filter, err := parseSearchResourcesFilter(request.Filter)
  98. if err != nil {
  99. return nil, status.Errorf(codes.InvalidArgument, "failed to parse filter: %v", err)
  100. }
  101. resourceFind := &store.FindResource{}
  102. if filter.UID != nil {
  103. resourceFind.UID = filter.UID
  104. }
  105. user, err := s.GetCurrentUser(ctx)
  106. if err != nil {
  107. return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
  108. }
  109. resourceFind.CreatorID = &user.ID
  110. resources, err := s.Store.ListResources(ctx, resourceFind)
  111. if err != nil {
  112. return nil, status.Errorf(codes.Internal, "failed to search resources: %v", err)
  113. }
  114. response := &v1pb.SearchResourcesResponse{}
  115. for _, resource := range resources {
  116. response.Resources = append(response.Resources, s.convertResourceFromStore(ctx, resource))
  117. }
  118. return response, nil
  119. }
  120. func (s *APIV1Service) GetResource(ctx context.Context, request *v1pb.GetResourceRequest) (*v1pb.Resource, error) {
  121. id, err := ExtractResourceIDFromName(request.Name)
  122. if err != nil {
  123. return nil, status.Errorf(codes.InvalidArgument, "invalid resource id: %v", err)
  124. }
  125. resource, err := s.Store.GetResource(ctx, &store.FindResource{
  126. ID: &id,
  127. })
  128. if err != nil {
  129. return nil, status.Errorf(codes.Internal, "failed to get resource: %v", err)
  130. }
  131. if resource == nil {
  132. return nil, status.Errorf(codes.NotFound, "resource not found")
  133. }
  134. return s.convertResourceFromStore(ctx, resource), nil
  135. }
  136. func (s *APIV1Service) GetResourceBinary(ctx context.Context, request *v1pb.GetResourceBinaryRequest) (*httpbody.HttpBody, error) {
  137. resourceFind := &store.FindResource{
  138. GetBlob: true,
  139. }
  140. if request.Name != "" {
  141. id, err := ExtractResourceIDFromName(request.Name)
  142. if err != nil {
  143. return nil, status.Errorf(codes.InvalidArgument, "invalid resource id: %v", err)
  144. }
  145. resourceFind.ID = &id
  146. }
  147. resource, err := s.Store.GetResource(ctx, resourceFind)
  148. if err != nil {
  149. return nil, status.Errorf(codes.Internal, "failed to get resource: %v", err)
  150. }
  151. if resource == nil {
  152. return nil, status.Errorf(codes.NotFound, "resource not found")
  153. }
  154. // Check the related memo visibility.
  155. if resource.MemoID != nil {
  156. memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
  157. ID: resource.MemoID,
  158. })
  159. if err != nil {
  160. return nil, status.Errorf(codes.Internal, "failed to find memo by ID: %v", resource.MemoID)
  161. }
  162. if memo != nil && memo.Visibility != store.Public {
  163. user, err := s.GetCurrentUser(ctx)
  164. if err != nil {
  165. return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
  166. }
  167. if user == nil {
  168. return nil, status.Errorf(codes.Unauthenticated, "unauthorized access")
  169. }
  170. if memo.Visibility == store.Private && user.ID != resource.CreatorID {
  171. return nil, status.Errorf(codes.Unauthenticated, "unauthorized access")
  172. }
  173. }
  174. }
  175. blob := resource.Blob
  176. if resource.StorageType == storepb.ResourceStorageType_LOCAL {
  177. resourcePath := filepath.FromSlash(resource.Reference)
  178. if !filepath.IsAbs(resourcePath) {
  179. resourcePath = filepath.Join(s.Profile.Data, resourcePath)
  180. }
  181. file, err := os.Open(resourcePath)
  182. if err != nil {
  183. if os.IsNotExist(err) {
  184. return nil, status.Errorf(codes.NotFound, "file not found for resource: %s", request.Name)
  185. }
  186. return nil, status.Errorf(codes.Internal, "failed to open the file: %v", err)
  187. }
  188. defer file.Close()
  189. blob, err = io.ReadAll(file)
  190. if err != nil {
  191. return nil, status.Errorf(codes.Internal, "failed to read the file: %v", err)
  192. }
  193. }
  194. contentType := resource.Type
  195. if strings.HasPrefix(contentType, "text/") {
  196. contentType += "; charset=utf-8"
  197. }
  198. httpBody := &httpbody.HttpBody{
  199. ContentType: contentType,
  200. Data: blob,
  201. }
  202. return httpBody, nil
  203. }
  204. func (s *APIV1Service) UpdateResource(ctx context.Context, request *v1pb.UpdateResourceRequest) (*v1pb.Resource, error) {
  205. id, err := ExtractResourceIDFromName(request.Resource.Name)
  206. if err != nil {
  207. return nil, status.Errorf(codes.InvalidArgument, "invalid resource id: %v", err)
  208. }
  209. if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
  210. return nil, status.Errorf(codes.InvalidArgument, "update mask is required")
  211. }
  212. currentTs := time.Now().Unix()
  213. update := &store.UpdateResource{
  214. ID: id,
  215. UpdatedTs: &currentTs,
  216. }
  217. for _, field := range request.UpdateMask.Paths {
  218. if field == "filename" {
  219. update.Filename = &request.Resource.Filename
  220. } else if field == "memo" {
  221. if request.Resource.Memo == nil {
  222. return nil, status.Errorf(codes.InvalidArgument, "memo is required")
  223. }
  224. memoID, err := ExtractMemoIDFromName(*request.Resource.Memo)
  225. if err != nil {
  226. return nil, status.Errorf(codes.InvalidArgument, "invalid memo id: %v", err)
  227. }
  228. update.MemoID = &memoID
  229. }
  230. }
  231. if err := s.Store.UpdateResource(ctx, update); err != nil {
  232. return nil, status.Errorf(codes.Internal, "failed to update resource: %v", err)
  233. }
  234. return s.GetResource(ctx, &v1pb.GetResourceRequest{
  235. Name: request.Resource.Name,
  236. })
  237. }
  238. func (s *APIV1Service) DeleteResource(ctx context.Context, request *v1pb.DeleteResourceRequest) (*emptypb.Empty, error) {
  239. id, err := ExtractResourceIDFromName(request.Name)
  240. if err != nil {
  241. return nil, status.Errorf(codes.InvalidArgument, "invalid resource id: %v", err)
  242. }
  243. user, err := s.GetCurrentUser(ctx)
  244. if err != nil {
  245. return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
  246. }
  247. resource, err := s.Store.GetResource(ctx, &store.FindResource{
  248. ID: &id,
  249. CreatorID: &user.ID,
  250. })
  251. if err != nil {
  252. return nil, status.Errorf(codes.Internal, "failed to find resource: %v", err)
  253. }
  254. if resource == nil {
  255. return nil, status.Errorf(codes.NotFound, "resource not found")
  256. }
  257. // Delete the resource from the database.
  258. if err := s.Store.DeleteResource(ctx, &store.DeleteResource{
  259. ID: resource.ID,
  260. }); err != nil {
  261. return nil, status.Errorf(codes.Internal, "failed to delete resource: %v", err)
  262. }
  263. return &emptypb.Empty{}, nil
  264. }
  265. func (s *APIV1Service) convertResourceFromStore(ctx context.Context, resource *store.Resource) *v1pb.Resource {
  266. resourceMessage := &v1pb.Resource{
  267. Name: fmt.Sprintf("%s%d", ResourceNamePrefix, resource.ID),
  268. Uid: resource.UID,
  269. CreateTime: timestamppb.New(time.Unix(resource.CreatedTs, 0)),
  270. Filename: resource.Filename,
  271. Type: resource.Type,
  272. Size: resource.Size,
  273. }
  274. if resource.StorageType == storepb.ResourceStorageType_EXTERNAL || resource.StorageType == storepb.ResourceStorageType_S3 {
  275. resourceMessage.ExternalLink = resource.Reference
  276. }
  277. if resource.MemoID != nil {
  278. memo, _ := s.Store.GetMemo(ctx, &store.FindMemo{
  279. ID: resource.MemoID,
  280. })
  281. if memo != nil {
  282. memoName := fmt.Sprintf("%s%d", MemoNamePrefix, memo.ID)
  283. resourceMessage.Memo = &memoName
  284. }
  285. }
  286. return resourceMessage
  287. }
  288. // SaveResourceBlob save the blob of resource based on the storage config.
  289. func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resource) error {
  290. workspaceStorageSetting, err := s.GetWorkspaceStorageSetting(ctx)
  291. if err != nil {
  292. return errors.Wrap(err, "Failed to find workspace storage setting")
  293. }
  294. if workspaceStorageSetting.StorageType == storepb.WorkspaceStorageSetting_LOCAL {
  295. filepathTemplate := "assets/{timestamp}_{filename}"
  296. if workspaceStorageSetting.FilepathTemplate != "" {
  297. filepathTemplate = workspaceStorageSetting.FilepathTemplate
  298. }
  299. internalPath := filepathTemplate
  300. if !strings.Contains(internalPath, "{filename}") {
  301. internalPath = filepath.Join(internalPath, "{filename}")
  302. }
  303. internalPath = replaceFilenameWithPathTemplate(internalPath, create.Filename)
  304. internalPath = filepath.ToSlash(internalPath)
  305. // Ensure the directory exists.
  306. osPath := filepath.FromSlash(internalPath)
  307. if !filepath.IsAbs(osPath) {
  308. osPath = filepath.Join(s.Profile.Data, osPath)
  309. }
  310. dir := filepath.Dir(osPath)
  311. if err = os.MkdirAll(dir, os.ModePerm); err != nil {
  312. return errors.Wrap(err, "Failed to create directory")
  313. }
  314. dst, err := os.Create(osPath)
  315. if err != nil {
  316. return errors.Wrap(err, "Failed to create file")
  317. }
  318. defer dst.Close()
  319. // Write the blob to the file.
  320. if err := os.WriteFile(osPath, create.Blob, 0644); err != nil {
  321. return errors.Wrap(err, "Failed to write file")
  322. }
  323. create.Reference = internalPath
  324. create.Blob = nil
  325. create.StorageType = storepb.ResourceStorageType_LOCAL
  326. } else if workspaceStorageSetting.StorageType == storepb.WorkspaceStorageSetting_S3 {
  327. s3Config := workspaceStorageSetting.S3Config
  328. if s3Config == nil {
  329. return errors.Errorf("No actived external storage found")
  330. }
  331. s3Client, err := s3.NewClient(ctx, s3Config)
  332. if err != nil {
  333. return errors.Wrap(err, "Failed to create s3 client")
  334. }
  335. filepathTemplate := workspaceStorageSetting.FilepathTemplate
  336. if !strings.Contains(filepathTemplate, "{filename}") {
  337. filepathTemplate = filepath.Join(filepathTemplate, "{filename}")
  338. }
  339. filepathTemplate = replaceFilenameWithPathTemplate(filepathTemplate, create.Filename)
  340. key, err := s3Client.UploadObject(ctx, filepathTemplate, create.Type, bytes.NewReader(create.Blob))
  341. if err != nil {
  342. return errors.Wrap(err, "Failed to upload via s3 client")
  343. }
  344. presignURL, err := s3Client.PresignGetObject(ctx, key)
  345. if err != nil {
  346. return errors.Wrap(err, "Failed to presign via s3 client")
  347. }
  348. create.Reference = presignURL
  349. create.Blob = nil
  350. create.StorageType = storepb.ResourceStorageType_S3
  351. create.Payload = &storepb.ResourcePayload{
  352. Payload: &storepb.ResourcePayload_S3Object_{
  353. S3Object: &storepb.ResourcePayload_S3Object{
  354. S3Config: s3Config,
  355. Key: key,
  356. LastPresignedTime: timestamppb.New(time.Now()),
  357. },
  358. },
  359. }
  360. }
  361. return nil
  362. }
  363. var fileKeyPattern = regexp.MustCompile(`\{[a-z]{1,9}\}`)
  364. func replaceFilenameWithPathTemplate(path, filename string) string {
  365. t := time.Now()
  366. path = fileKeyPattern.ReplaceAllStringFunc(path, func(s string) string {
  367. switch s {
  368. case "{filename}":
  369. return filename
  370. case "{timestamp}":
  371. return fmt.Sprintf("%d", t.Unix())
  372. case "{year}":
  373. return fmt.Sprintf("%d", t.Year())
  374. case "{month}":
  375. return fmt.Sprintf("%02d", t.Month())
  376. case "{day}":
  377. return fmt.Sprintf("%02d", t.Day())
  378. case "{hour}":
  379. return fmt.Sprintf("%02d", t.Hour())
  380. case "{minute}":
  381. return fmt.Sprintf("%02d", t.Minute())
  382. case "{second}":
  383. return fmt.Sprintf("%02d", t.Second())
  384. case "{uuid}":
  385. return util.GenUUID()
  386. }
  387. return s
  388. })
  389. return path
  390. }
  391. // SearchResourcesFilterCELAttributes are the CEL attributes for SearchResourcesFilter.
  392. var SearchResourcesFilterCELAttributes = []cel.EnvOption{
  393. cel.Variable("uid", cel.StringType),
  394. }
  395. type SearchResourcesFilter struct {
  396. UID *string
  397. }
  398. func parseSearchResourcesFilter(expression string) (*SearchResourcesFilter, error) {
  399. e, err := cel.NewEnv(SearchResourcesFilterCELAttributes...)
  400. if err != nil {
  401. return nil, err
  402. }
  403. ast, issues := e.Compile(expression)
  404. if issues != nil {
  405. return nil, errors.Errorf("found issue %v", issues)
  406. }
  407. filter := &SearchResourcesFilter{}
  408. expr, err := cel.AstToParsedExpr(ast)
  409. if err != nil {
  410. return nil, err
  411. }
  412. callExpr := expr.GetExpr().GetCallExpr()
  413. findSearchResourcesField(callExpr, filter)
  414. return filter, nil
  415. }
  416. func findSearchResourcesField(callExpr *expr.Expr_Call, filter *SearchResourcesFilter) {
  417. if len(callExpr.Args) == 2 {
  418. idExpr := callExpr.Args[0].GetIdentExpr()
  419. if idExpr != nil {
  420. if idExpr.Name == "uid" {
  421. uid := callExpr.Args[1].GetConstExpr().GetStringValue()
  422. filter.UID = &uid
  423. }
  424. return
  425. }
  426. }
  427. for _, arg := range callExpr.Args {
  428. callExpr := arg.GetCallExpr()
  429. if callExpr != nil {
  430. findSearchResourcesField(callExpr, filter)
  431. }
  432. }
  433. }