package services import ( "context" "errors" "git.aiterp.net/rpdata/api/internal/generate" "git.aiterp.net/rpdata/api/models" "git.aiterp.net/rpdata/api/models/changekeys" "git.aiterp.net/rpdata/api/repositories" "git.aiterp.net/rpdata/api/space" "github.com/h2non/filetype" "io" "log" "strings" "time" ) var ErrPrivateNoAuthor = errors.New("cannot search for private files without an author") var ErrInvalidName = errors.New("invalid name") var ErrInvalidFileSize = errors.New("file is not of a correct size (min: 320B, max: 16MB)") var ErrCouldNotReadHead = errors.New("could not read file head") var ErrInvalidFileType = errors.New("file type could not be recognized or is not allowed") var ErrCouldNotUploadFile = errors.New("could not upload file") // FileService is a service for files. type FileService struct { files repositories.FileRepository authService *AuthService changeService *ChangeService space *space.Client } func (s *FileService) Find(ctx context.Context, id string) (*models.File, error) { file, err := s.files.Find(ctx, id) if err != nil { return nil, err } if !file.Public { err := s.authService.CheckPermission(ctx, "view", file) if err != nil { return nil, repositories.ErrNotFound } } return file, nil } func (s *FileService) List(ctx context.Context, filter models.FileFilter) ([]*models.File, error) { token := s.authService.TokenFromContext(ctx) if filter.Public != nil { if *filter.Public == false { if filter.Author == nil || *filter.Author == "" { if token == nil { return nil, ErrUnauthorized } filter.Author = &token.UserID } else if !token.PermittedUser(*filter.Author, "member", "file.list") { return nil, ErrUnauthorized } } } return s.files.List(ctx, filter) } func (s *FileService) Upload(ctx context.Context, reader io.Reader, name string, public bool, size int64) (*models.File, error) { if name == "" { return nil, ErrInvalidName } else if size < 320 || size > 16777216 { return nil, ErrInvalidFileSize } token := s.authService.TokenFromContext(ctx) if token == nil || !token.Permitted("file.upload", "member") { return nil, ErrUnauthorized } head := make([]byte, 320) n, err := reader.Read(head) if err != nil || n < 320 { return nil, ErrCouldNotReadHead } fileType, err := filetype.Match(head) if err != nil || !allowedMimeTypes[fileType.MIME.Value] { return nil, ErrInvalidFileType } reader2 := &concatReader{head: head, body: reader} path := generate.FileUploadID() + "." + fileType.Extension err = s.space.UploadFile(ctx, path, fileType.MIME.Value, reader2, size) if err != nil || !allowedMimeTypes[fileType.MIME.Value] { log.Println("File upload failed:", err) return nil, ErrCouldNotUploadFile } file := &models.File{ Size: size, Name: name, URL: s.space.URLFromPath(path), Time: time.Now(), Author: token.UserID, Kind: "upload", MimeType: fileType.MIME.Value, Public: public, } file, err = s.files.Insert(ctx, *file) if err != nil { return nil, err } s.changeService.Submit(ctx, models.ChangeModelFile, "upload", file.Public, changekeys.Listed(file), file) return file, nil } func (s *FileService) Edit(ctx context.Context, id string, name *string, public *bool) (*models.File, error) { file, err := s.files.Find(ctx, id) if err != nil { return nil, err } err = s.authService.CheckPermission(ctx, "edit", file) if err != nil { return nil, err } file, err = s.files.Update(ctx, *file, models.FileUpdate{ Name: name, Public: public, }) if err != nil { return nil, err } s.changeService.Submit(ctx, models.ChangeModelFile, "edit", file.Public, changekeys.Listed(file), file) return file, nil } func (s *FileService) Remove(ctx context.Context, id string) (*models.File, error) { file, err := s.files.Find(ctx, id) if err != nil { return nil, err } err = s.authService.CheckPermission(ctx, "remove", file) if err != nil { return nil, err } if file.Kind == "upload" { split := strings.Split(file.URL, "/") name := split[len(split)-1] if strings.HasPrefix(name, "U") { err := s.space.RemoveFile(name) if err != nil { log.Printf("Failed to remove file %s for id %s: %s", name, id, err) } } } err = s.files.Delete(ctx, *file) if err != nil { return nil, err } s.changeService.Submit(ctx, models.ChangeModelFile, "remove", file.Public, changekeys.Listed(file), file) return file, nil } // concatReader is a quick and dirty reader for reading the head and the file. type concatReader struct { head []byte headPos int body io.Reader } func (r *concatReader) Read(p []byte) (n int, err error) { if len(p) == 0 { return 0, io.ErrShortBuffer } if r.headPos < len(r.head) { remainder := r.head[r.headPos:] if len(p) < len(remainder) { r.headPos += len(p) copy(p, remainder) return len(p), nil } r.headPos = len(r.head) copy(p, remainder) return len(remainder), nil } return r.body.Read(p) } var allowedMimeTypes = map[string]bool{ "": false, "image/jpeg": true, "image/png": true, "image/gif": true, "image/tiff": true, "image/tga": true, "text/plain": true, "application/json": true, "application/pdf": false, "binary/octet-stream": false, "video/mp4": false, "audio/mp3": false, }