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
223 lines
5.3 KiB
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,
|
|
}
|