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.

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