resource_service.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  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.Filename != nil {
  103. resourceFind.FilenameSearch = filter.Filename
  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. //nolint:all
  137. func (s *APIV1Service) GetResourceByUid(ctx context.Context, request *v1pb.GetResourceByUidRequest) (*v1pb.Resource, error) {
  138. resource, err := s.Store.GetResource(ctx, &store.FindResource{
  139. UID: &request.Uid,
  140. })
  141. if err != nil {
  142. return nil, status.Errorf(codes.Internal, "failed to get resource: %v", err)
  143. }
  144. if resource == nil {
  145. return nil, status.Errorf(codes.NotFound, "resource not found")
  146. }
  147. return s.convertResourceFromStore(ctx, resource), nil
  148. }
  149. func (s *APIV1Service) GetResourceBinary(ctx context.Context, request *v1pb.GetResourceBinaryRequest) (*httpbody.HttpBody, error) {
  150. resourceFind := &store.FindResource{
  151. GetBlob: true,
  152. }
  153. if request.Name != "" {
  154. id, err := ExtractResourceIDFromName(request.Name)
  155. if err != nil {
  156. return nil, status.Errorf(codes.InvalidArgument, "invalid resource id: %v", err)
  157. }
  158. resourceFind.ID = &id
  159. }
  160. resource, err := s.Store.GetResource(ctx, resourceFind)
  161. if err != nil {
  162. return nil, status.Errorf(codes.Internal, "failed to get resource: %v", err)
  163. }
  164. if resource == nil {
  165. return nil, status.Errorf(codes.NotFound, "resource not found")
  166. }
  167. // Check the related memo visibility.
  168. if resource.MemoID != nil {
  169. memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
  170. ID: resource.MemoID,
  171. })
  172. if err != nil {
  173. return nil, status.Errorf(codes.Internal, "failed to find memo by ID: %v", resource.MemoID)
  174. }
  175. if memo != nil && memo.Visibility != store.Public {
  176. user, err := s.GetCurrentUser(ctx)
  177. if err != nil {
  178. return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
  179. }
  180. if user == nil {
  181. return nil, status.Errorf(codes.Unauthenticated, "unauthorized access")
  182. }
  183. if memo.Visibility == store.Private && user.ID != resource.CreatorID {
  184. return nil, status.Errorf(codes.Unauthenticated, "unauthorized access")
  185. }
  186. }
  187. }
  188. blob := resource.Blob
  189. if resource.StorageType == storepb.ResourceStorageType_LOCAL {
  190. resourcePath := filepath.FromSlash(resource.Reference)
  191. if !filepath.IsAbs(resourcePath) {
  192. resourcePath = filepath.Join(s.Profile.Data, resourcePath)
  193. }
  194. file, err := os.Open(resourcePath)
  195. if err != nil {
  196. if os.IsNotExist(err) {
  197. return nil, status.Errorf(codes.NotFound, "file not found for resource: %s", request.Name)
  198. }
  199. return nil, status.Errorf(codes.Internal, "failed to open the file: %v", err)
  200. }
  201. defer file.Close()
  202. blob, err = io.ReadAll(file)
  203. if err != nil {
  204. return nil, status.Errorf(codes.Internal, "failed to read the file: %v", err)
  205. }
  206. }
  207. contentType := resource.Type
  208. if strings.HasPrefix(contentType, "text/") {
  209. contentType += "; charset=utf-8"
  210. }
  211. httpBody := &httpbody.HttpBody{
  212. ContentType: contentType,
  213. Data: blob,
  214. }
  215. return httpBody, nil
  216. }
  217. func (s *APIV1Service) UpdateResource(ctx context.Context, request *v1pb.UpdateResourceRequest) (*v1pb.Resource, error) {
  218. id, err := ExtractResourceIDFromName(request.Resource.Name)
  219. if err != nil {
  220. return nil, status.Errorf(codes.InvalidArgument, "invalid resource id: %v", err)
  221. }
  222. if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
  223. return nil, status.Errorf(codes.InvalidArgument, "update mask is required")
  224. }
  225. currentTs := time.Now().Unix()
  226. update := &store.UpdateResource{
  227. ID: id,
  228. UpdatedTs: &currentTs,
  229. }
  230. for _, field := range request.UpdateMask.Paths {
  231. if field == "filename" {
  232. update.Filename = &request.Resource.Filename
  233. } else if field == "memo" {
  234. if request.Resource.Memo == nil {
  235. return nil, status.Errorf(codes.InvalidArgument, "memo is required")
  236. }
  237. memoID, err := ExtractMemoIDFromName(*request.Resource.Memo)
  238. if err != nil {
  239. return nil, status.Errorf(codes.InvalidArgument, "invalid memo id: %v", err)
  240. }
  241. update.MemoID = &memoID
  242. }
  243. }
  244. if err := s.Store.UpdateResource(ctx, update); err != nil {
  245. return nil, status.Errorf(codes.Internal, "failed to update resource: %v", err)
  246. }
  247. return s.GetResource(ctx, &v1pb.GetResourceRequest{
  248. Name: request.Resource.Name,
  249. })
  250. }
  251. func (s *APIV1Service) DeleteResource(ctx context.Context, request *v1pb.DeleteResourceRequest) (*emptypb.Empty, error) {
  252. id, err := ExtractResourceIDFromName(request.Name)
  253. if err != nil {
  254. return nil, status.Errorf(codes.InvalidArgument, "invalid resource id: %v", err)
  255. }
  256. user, err := s.GetCurrentUser(ctx)
  257. if err != nil {
  258. return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
  259. }
  260. resource, err := s.Store.GetResource(ctx, &store.FindResource{
  261. ID: &id,
  262. CreatorID: &user.ID,
  263. })
  264. if err != nil {
  265. return nil, status.Errorf(codes.Internal, "failed to find resource: %v", err)
  266. }
  267. if resource == nil {
  268. return nil, status.Errorf(codes.NotFound, "resource not found")
  269. }
  270. // Delete the resource from the database.
  271. if err := s.Store.DeleteResource(ctx, &store.DeleteResource{
  272. ID: resource.ID,
  273. }); err != nil {
  274. return nil, status.Errorf(codes.Internal, "failed to delete resource: %v", err)
  275. }
  276. return &emptypb.Empty{}, nil
  277. }
  278. func (s *APIV1Service) convertResourceFromStore(ctx context.Context, resource *store.Resource) *v1pb.Resource {
  279. resourceMessage := &v1pb.Resource{
  280. Name: fmt.Sprintf("%s%d", ResourceNamePrefix, resource.ID),
  281. Uid: resource.UID,
  282. CreateTime: timestamppb.New(time.Unix(resource.CreatedTs, 0)),
  283. Filename: resource.Filename,
  284. Type: resource.Type,
  285. Size: resource.Size,
  286. }
  287. if resource.StorageType == storepb.ResourceStorageType_EXTERNAL || resource.StorageType == storepb.ResourceStorageType_S3 {
  288. resourceMessage.ExternalLink = resource.Reference
  289. }
  290. if resource.MemoID != nil {
  291. memo, _ := s.Store.GetMemo(ctx, &store.FindMemo{
  292. ID: resource.MemoID,
  293. })
  294. if memo != nil {
  295. memoName := fmt.Sprintf("%s%d", MemoNamePrefix, memo.ID)
  296. resourceMessage.Memo = &memoName
  297. }
  298. }
  299. return resourceMessage
  300. }
  301. // SaveResourceBlob save the blob of resource based on the storage config.
  302. func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resource) error {
  303. workspaceStorageSetting, err := s.GetWorkspaceStorageSetting(ctx)
  304. if err != nil {
  305. return errors.Wrap(err, "Failed to find workspace storage setting")
  306. }
  307. if workspaceStorageSetting.StorageType == storepb.WorkspaceStorageSetting_LOCAL {
  308. filepathTemplate := "assets/{timestamp}_{filename}"
  309. if workspaceStorageSetting.FilepathTemplate != "" {
  310. filepathTemplate = workspaceStorageSetting.FilepathTemplate
  311. }
  312. internalPath := filepathTemplate
  313. if !strings.Contains(internalPath, "{filename}") {
  314. internalPath = filepath.Join(internalPath, "{filename}")
  315. }
  316. internalPath = replaceFilenameWithPathTemplate(internalPath, create.Filename)
  317. internalPath = filepath.ToSlash(internalPath)
  318. // Ensure the directory exists.
  319. osPath := filepath.FromSlash(internalPath)
  320. if !filepath.IsAbs(osPath) {
  321. osPath = filepath.Join(s.Profile.Data, osPath)
  322. }
  323. dir := filepath.Dir(osPath)
  324. if err = os.MkdirAll(dir, os.ModePerm); err != nil {
  325. return errors.Wrap(err, "Failed to create directory")
  326. }
  327. dst, err := os.Create(osPath)
  328. if err != nil {
  329. return errors.Wrap(err, "Failed to create file")
  330. }
  331. defer dst.Close()
  332. // Write the blob to the file.
  333. if err := os.WriteFile(osPath, create.Blob, 0644); err != nil {
  334. return errors.Wrap(err, "Failed to write file")
  335. }
  336. create.Reference = internalPath
  337. create.Blob = nil
  338. create.StorageType = storepb.ResourceStorageType_LOCAL
  339. } else if workspaceStorageSetting.StorageType == storepb.WorkspaceStorageSetting_S3 {
  340. s3Config := workspaceStorageSetting.S3Config
  341. if s3Config == nil {
  342. return errors.Errorf("No actived external storage found")
  343. }
  344. s3Client, err := s3.NewClient(ctx, s3Config)
  345. if err != nil {
  346. return errors.Wrap(err, "Failed to create s3 client")
  347. }
  348. filepathTemplate := workspaceStorageSetting.FilepathTemplate
  349. if !strings.Contains(filepathTemplate, "{filename}") {
  350. filepathTemplate = filepath.Join(filepathTemplate, "{filename}")
  351. }
  352. filepathTemplate = replaceFilenameWithPathTemplate(filepathTemplate, create.Filename)
  353. key, err := s3Client.UploadObject(ctx, filepathTemplate, create.Type, bytes.NewReader(create.Blob))
  354. if err != nil {
  355. return errors.Wrap(err, "Failed to upload via s3 client")
  356. }
  357. presignURL, err := s3Client.PresignGetObject(ctx, key)
  358. if err != nil {
  359. return errors.Wrap(err, "Failed to presign via s3 client")
  360. }
  361. create.Reference = presignURL
  362. create.Blob = nil
  363. create.StorageType = storepb.ResourceStorageType_S3
  364. create.Payload = &storepb.ResourcePayload{
  365. Payload: &storepb.ResourcePayload_S3Object_{
  366. S3Object: &storepb.ResourcePayload_S3Object{
  367. S3Config: s3Config,
  368. Key: key,
  369. LastPresignedTime: timestamppb.New(time.Now()),
  370. },
  371. },
  372. }
  373. }
  374. return nil
  375. }
  376. var fileKeyPattern = regexp.MustCompile(`\{[a-z]{1,9}\}`)
  377. func replaceFilenameWithPathTemplate(path, filename string) string {
  378. t := time.Now()
  379. path = fileKeyPattern.ReplaceAllStringFunc(path, func(s string) string {
  380. switch s {
  381. case "{filename}":
  382. return filename
  383. case "{timestamp}":
  384. return fmt.Sprintf("%d", t.Unix())
  385. case "{year}":
  386. return fmt.Sprintf("%d", t.Year())
  387. case "{month}":
  388. return fmt.Sprintf("%02d", t.Month())
  389. case "{day}":
  390. return fmt.Sprintf("%02d", t.Day())
  391. case "{hour}":
  392. return fmt.Sprintf("%02d", t.Hour())
  393. case "{minute}":
  394. return fmt.Sprintf("%02d", t.Minute())
  395. case "{second}":
  396. return fmt.Sprintf("%02d", t.Second())
  397. case "{uuid}":
  398. return util.GenUUID()
  399. }
  400. return s
  401. })
  402. return path
  403. }
  404. // SearchResourcesFilterCELAttributes are the CEL attributes for SearchResourcesFilter.
  405. var SearchResourcesFilterCELAttributes = []cel.EnvOption{
  406. cel.Variable("filename", cel.StringType),
  407. }
  408. type SearchResourcesFilter struct {
  409. Filename *string
  410. }
  411. func parseSearchResourcesFilter(expression string) (*SearchResourcesFilter, error) {
  412. e, err := cel.NewEnv(SearchResourcesFilterCELAttributes...)
  413. if err != nil {
  414. return nil, err
  415. }
  416. ast, issues := e.Compile(expression)
  417. if issues != nil {
  418. return nil, errors.Errorf("found issue %v", issues)
  419. }
  420. filter := &SearchResourcesFilter{}
  421. expr, err := cel.AstToParsedExpr(ast)
  422. if err != nil {
  423. return nil, err
  424. }
  425. callExpr := expr.GetExpr().GetCallExpr()
  426. findSearchResourcesField(callExpr, filter)
  427. return filter, nil
  428. }
  429. func findSearchResourcesField(callExpr *expr.Expr_Call, filter *SearchResourcesFilter) {
  430. if len(callExpr.Args) == 2 {
  431. idExpr := callExpr.Args[0].GetIdentExpr()
  432. if idExpr != nil {
  433. if idExpr.Name == "filename" {
  434. filename := callExpr.Args[1].GetConstExpr().GetStringValue()
  435. filter.Filename = &filename
  436. }
  437. return
  438. }
  439. }
  440. for _, arg := range callExpr.Args {
  441. callExpr := arg.GetCallExpr()
  442. if callExpr != nil {
  443. findSearchResourcesField(callExpr, filter)
  444. }
  445. }
  446. }