GraphQL API and utilities for the rpdata project
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

144 lines
3.3 KiB

  1. package services
  2. import (
  3. "context"
  4. "errors"
  5. "git.aiterp.net/rpdata/api/internal/auth"
  6. "git.aiterp.net/rpdata/api/models"
  7. "git.aiterp.net/rpdata/api/repositories"
  8. "github.com/h2non/filetype"
  9. "io"
  10. )
  11. var ErrPrivateNoAuthor = errors.New("cannot search for private files without an author")
  12. var ErrInvalidName = errors.New("invalid name")
  13. var ErrInvalidFileSize = errors.New("file is not of a correct size (min: 320B, max: 16MB)")
  14. var ErrCouldNotReadHead = errors.New("could not read file head")
  15. var ErrInvalidFileType = errors.New("file type could not be recognized or is not allowed")
  16. var ErrCouldNotUploadFile = errors.New("could not upload file")
  17. // FileService is a service for files.
  18. type FileService struct {
  19. files repositories.FileRepository
  20. }
  21. func (s *FileService) Find(ctx context.Context, id string) (*models.File, error) {
  22. file, err := s.files.Find(ctx, id)
  23. if err != nil {
  24. return nil, err
  25. }
  26. if !file.Public {
  27. err := auth.CheckPermission(ctx, "view", file)
  28. if err != nil {
  29. return nil, repositories.ErrNotFound
  30. }
  31. }
  32. return file, nil
  33. }
  34. func (s *FileService) List(ctx context.Context, filter models.FileFilter) ([]*models.File, error) {
  35. token := auth.TokenFromContext(ctx)
  36. if filter.Public != nil {
  37. if *filter.Public == false {
  38. if filter.Author == nil || *filter.Author == "" {
  39. return nil, ErrPrivateNoAuthor
  40. }
  41. if !token.PermittedUser(*filter.Author, "member", "file.list") {
  42. return nil, auth.ErrUnauthorized
  43. }
  44. }
  45. }
  46. return s.files.List(ctx, filter)
  47. }
  48. func (s *FileService) Upload(ctx context.Context, reader io.Reader, name string, size int64) (*models.File, error) {
  49. if name == "" {
  50. return nil, ErrInvalidName
  51. } else if size < 320 || size > 16777216 {
  52. return nil, ErrInvalidFileSize
  53. }
  54. head := make([]byte, 320)
  55. n, err := reader.Read(head)
  56. if err != nil || n < 320 {
  57. return nil, ErrCouldNotReadHead
  58. }
  59. fileType, err := filetype.Match(head)
  60. if err != nil || !allowedMimeTypes[fileType.MIME.Value] {
  61. return nil, ErrInvalidFileType
  62. }
  63. panic("implement rest of me")
  64. }
  65. func (s *FileService) Edit(ctx context.Context, id string, name *string, public *bool) (*models.File, error) {
  66. file, err := s.files.Find(ctx, id)
  67. if err != nil {
  68. return nil, err
  69. }
  70. err = auth.CheckPermission(ctx, "edit", file)
  71. if err != nil {
  72. if !file.Public {
  73. return nil, repositories.ErrNotFound
  74. }
  75. return nil, err
  76. }
  77. return s.files.Update(ctx, *file, models.FileUpdate{
  78. Name: name,
  79. Public: public,
  80. })
  81. }
  82. // concatReader is a quick and dirty reader for reading the head and the file.
  83. type concatReader struct {
  84. head []byte
  85. headPos int
  86. body io.Reader
  87. }
  88. func (r *concatReader) Read(p []byte) (n int, err error) {
  89. if len(p) == 0 {
  90. return 0, io.ErrShortBuffer
  91. }
  92. if r.headPos < len(r.head) {
  93. remainder := r.head[r.headPos:]
  94. if len(p) < len(remainder) {
  95. r.headPos += len(p)
  96. copy(p, remainder)
  97. return len(p), nil
  98. }
  99. r.headPos = len(r.head)
  100. copy(p, remainder)
  101. return len(p), nil
  102. }
  103. return r.body.Read(p)
  104. }
  105. var allowedMimeTypes = map[string]bool{
  106. "": false,
  107. "image/jpeg": true,
  108. "image/png": true,
  109. "image/gif": true,
  110. "image/tiff": true,
  111. "image/tga": true,
  112. "text/plain": true,
  113. "application/json": true,
  114. "application/pdf": false,
  115. "binary/octet-stream": false,
  116. "video/mp4": false,
  117. "audio/mp3": false,
  118. }