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.

184 lines
4.4 KiB

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