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.

252 lines
5.5 KiB

  1. package file
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "encoding/binary"
  6. "errors"
  7. "io"
  8. "strconv"
  9. "time"
  10. "git.aiterp.net/rpdata/api/internal/store"
  11. "github.com/globalsign/mgo"
  12. "github.com/globalsign/mgo/bson"
  13. )
  14. var fileCollection *mgo.Collection
  15. // A File is a record of a file stored in the Space.
  16. type File struct {
  17. ID string `bson:"_id" json:"id"`
  18. Time time.Time `bson:"time" json:"time"`
  19. Kind string `bson:"kind" json:"kind"`
  20. Public bool `bson:"public" json:"public"`
  21. Name string `bson:"name" json:"name"`
  22. MimeType string `bson:"mimeType" json:"mimeType"`
  23. Size int64 `bson:"size" json:"size"`
  24. Author string `bson:"author" json:"author"`
  25. URL string `bson:"url,omitempty" json:"url,omitempty"`
  26. }
  27. // Edit edits the file, changing up to both the two mutable properties
  28. func (file *File) Edit(name *string, public *bool) error {
  29. changes := bson.M{}
  30. changedFile := *file
  31. if name != nil && *name != file.Name {
  32. changes["name"] = *name
  33. changedFile.Name = *name
  34. }
  35. if public != nil && *public != file.Public {
  36. changes["public"] = *public
  37. changedFile.Public = *public
  38. }
  39. if len(changes) == 0 {
  40. return nil
  41. }
  42. err := fileCollection.UpdateId(file.ID, bson.M{"$set": changes})
  43. if err != nil {
  44. return err
  45. }
  46. *file = changedFile
  47. return nil
  48. }
  49. // Delete removes the file information from the database, and deletes the file.
  50. func (file *File) Delete() error {
  51. err := fileCollection.RemoveId(file.ID)
  52. if err != nil {
  53. return err
  54. }
  55. if file.Kind == "upload" {
  56. err = store.RemoveFile("files", file.ID)
  57. if err != nil {
  58. return err
  59. }
  60. }
  61. file.URL = ""
  62. return nil
  63. }
  64. // Insert manually inserts file information into the database. This should never, ever be API accessible
  65. func Insert(name, kind, mimeType, author string, time time.Time, size int64, url string) (File, error) {
  66. file := File{
  67. ID: makeFileID(),
  68. Kind: kind,
  69. Time: time,
  70. Public: false,
  71. Author: author,
  72. Name: name,
  73. MimeType: mimeType,
  74. Size: size,
  75. URL: url,
  76. }
  77. err := fileCollection.Insert(file)
  78. if err != nil {
  79. return File{}, err
  80. }
  81. return file, nil
  82. }
  83. // Upload adds a file to the space.
  84. func Upload(ctx context.Context, name, mimeType, author string, size int64, input io.Reader) (File, error) {
  85. if !allowdMimeTypes[mimeType] {
  86. return File{}, errors.New("File type not allowed:" + mimeType)
  87. }
  88. if name == "" {
  89. date := time.Now().UTC().Format("Jan 02 2006 15:04:05 MST")
  90. name = "Unnamed file (" + date + ")"
  91. }
  92. if mimeType == "" {
  93. mimeType = "binary/octet-stream"
  94. }
  95. id := makeFileID()
  96. path, err := store.UploadFile(ctx, "files", id, mimeType, input, size)
  97. if err != nil {
  98. return File{}, err
  99. }
  100. file := File{
  101. ID: id,
  102. Kind: "upload",
  103. Time: time.Now(),
  104. Public: false,
  105. Author: author,
  106. Name: name,
  107. MimeType: mimeType,
  108. Size: size,
  109. URL: store.URLFromPath(path),
  110. }
  111. err = fileCollection.Insert(file)
  112. if err != nil {
  113. return File{}, err
  114. }
  115. return file, nil
  116. }
  117. // FindID finds a file by ID
  118. func FindID(id string) (File, error) {
  119. file := File{}
  120. err := fileCollection.FindId(id).One(&file)
  121. if err != nil {
  122. return File{}, err
  123. }
  124. return file, nil
  125. }
  126. // FindName finds a file by kind, name and author
  127. func FindName(kind string, name string, author string) (File, error) {
  128. query := bson.M{"kind": kind, "name": name, "author": author}
  129. file := File{}
  130. err := fileCollection.Find(query).One(&file)
  131. if err != nil {
  132. return File{}, err
  133. }
  134. return file, nil
  135. }
  136. // List lists files according to the standard lookup. By default it's just the author's own files,
  137. // but if `public` is true it will alos include files made public by other authors. If `mimeTypes` contains
  138. // any, it will limit the results to that. If `author` is empty, it will only list public files
  139. func List(author string, public bool, mimeTypes []string) ([]File, error) {
  140. query := bson.M{}
  141. if author != "" {
  142. if public {
  143. query["$or"] = []bson.M{
  144. bson.M{"author": author},
  145. bson.M{"public": true},
  146. }
  147. } else {
  148. query["author"] = author
  149. }
  150. } else {
  151. if !public {
  152. return nil, errors.New("No author specified, and public is unset")
  153. }
  154. query["public"] = true
  155. }
  156. if len(mimeTypes) > 0 {
  157. query["mimeTypes"] = bson.M{"$in": mimeTypes}
  158. }
  159. return listFiles(query)
  160. }
  161. func listFiles(query interface{}) ([]File, error) {
  162. list := make([]File, 0, 32)
  163. err := fileCollection.Find(query).Sort("-time").All(&list)
  164. if err != nil {
  165. return nil, err
  166. }
  167. return list, nil
  168. }
  169. // makeFileID makes a random file ID that's 32 characters long
  170. func makeFileID() string {
  171. result := "F" + strconv.FormatInt(time.Now().UnixNano(), 36)
  172. offset := 0
  173. data := make([]byte, 32)
  174. rand.Read(data)
  175. for len(result) < 32 {
  176. result += strconv.FormatUint(binary.LittleEndian.Uint64(data[offset:]), 36)
  177. offset += 8
  178. if offset >= 32 {
  179. rand.Read(data)
  180. offset = 0
  181. }
  182. }
  183. return result[:32]
  184. }
  185. func init() {
  186. store.HandleInit(func(db *mgo.Database) {
  187. fileCollection = db.C("file.headers")
  188. fileCollection.EnsureIndexKey("author")
  189. fileCollection.EnsureIndexKey("public")
  190. fileCollection.EnsureIndexKey("kind", "name", "author")
  191. fileCollection.EnsureIndexKey("author", "public")
  192. fileCollection.EnsureIndexKey("kind")
  193. })
  194. }
  195. var allowdMimeTypes = map[string]bool{
  196. "": false,
  197. "image/jpeg": true,
  198. "image/png": true,
  199. "image/gif": true,
  200. "text/plain": true,
  201. "application/json": true,
  202. "application/pdf": false,
  203. "binary/octet-stream": false,
  204. "video/mp4": false,
  205. "audio/mp3": false,
  206. }