Gisle Aune
5 years ago
11 changed files with 394 additions and 1 deletions
-
1.idea/dictionaries/gisle.xml
-
12database/mongodb/db.go
-
105database/mongodb/files.go
-
1go.mod
-
2go.sum
-
2internal/auth/permissions.go
-
5internal/generate/id.go
-
13models/file.go
-
15repositories/file.go
-
144services/files.go
-
93space/space.go
@ -0,0 +1,105 @@ |
|||
package mongodb |
|||
|
|||
import ( |
|||
"context" |
|||
"git.aiterp.net/rpdata/api/internal/generate" |
|||
"git.aiterp.net/rpdata/api/models" |
|||
"git.aiterp.net/rpdata/api/repositories" |
|||
"github.com/globalsign/mgo" |
|||
"github.com/globalsign/mgo/bson" |
|||
) |
|||
|
|||
type fileRepository struct { |
|||
files *mgo.Collection |
|||
} |
|||
|
|||
func (r *fileRepository) Find(ctx context.Context, id string) (*models.File, error) { |
|||
file := new(models.File) |
|||
err := r.files.FindId(id).One(file) |
|||
if err != nil { |
|||
if err == mgo.ErrNotFound { |
|||
return nil, repositories.ErrNotFound |
|||
} |
|||
|
|||
return nil, err |
|||
} |
|||
|
|||
return file, nil |
|||
} |
|||
|
|||
func (r *fileRepository) List(ctx context.Context, filter models.FileFilter) ([]*models.File, error) { |
|||
query := bson.M{} |
|||
if filter.Author != nil { |
|||
query["author"] = *filter.Author |
|||
} |
|||
if filter.MimeTypes != nil { |
|||
query["mimeType"] = bson.M{"$in": filter.MimeTypes} |
|||
} |
|||
if filter.Public != nil { |
|||
query["public"] = *filter.Public |
|||
} |
|||
|
|||
files := make([]*models.File, 0, 16) |
|||
err := r.files.Find(query).Sort("-date").All(&files) |
|||
|
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return files, nil |
|||
} |
|||
|
|||
func (r *fileRepository) Insert(ctx context.Context, file models.File) (*models.File, error) { |
|||
file.ID = generate.FileID() |
|||
|
|||
err := r.files.Insert(&file) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return &file, nil |
|||
} |
|||
|
|||
func (r *fileRepository) Update(ctx context.Context, file models.File, update models.FileUpdate) (*models.File, error) { |
|||
updateBson := bson.M{} |
|||
if update.Public != nil { |
|||
updateBson["public"] = *update.Public |
|||
file.Public = *update.Public |
|||
} |
|||
if update.Name != nil { |
|||
updateBson["name"] = *update.Name |
|||
file.Name = *update.Name |
|||
} |
|||
|
|||
err := r.files.UpdateId(file.ID, bson.M{"$set": updateBson}) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return &file, nil |
|||
} |
|||
|
|||
func (r *fileRepository) Delete(ctx context.Context, file models.File) error { |
|||
return r.files.RemoveId(file.ID) |
|||
} |
|||
|
|||
func newFileRepository(db *mgo.Database) (*fileRepository, error) { |
|||
files := db.C("file.headers") |
|||
|
|||
err := files.EnsureIndexKey("author") |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
err = files.EnsureIndexKey("mimeType") |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
err = files.EnsureIndexKey("public") |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
return &fileRepository{files: files}, nil |
|||
} |
@ -0,0 +1,15 @@ |
|||
package repositories |
|||
|
|||
import ( |
|||
"context" |
|||
"git.aiterp.net/rpdata/api/models" |
|||
) |
|||
|
|||
// FileRepository is an interface for database operations relating to file metadata.
|
|||
type FileRepository interface { |
|||
Find(ctx context.Context, id string) (*models.File, error) |
|||
List(ctx context.Context, filter models.FileFilter) ([]*models.File, error) |
|||
Insert(ctx context.Context, file models.File) (*models.File, error) |
|||
Update(ctx context.Context, file models.File, update models.FileUpdate) (*models.File, error) |
|||
Delete(ctx context.Context, file models.File) error |
|||
} |
@ -0,0 +1,144 @@ |
|||
package services |
|||
|
|||
import ( |
|||
"context" |
|||
"errors" |
|||
"git.aiterp.net/rpdata/api/internal/auth" |
|||
"git.aiterp.net/rpdata/api/models" |
|||
"git.aiterp.net/rpdata/api/repositories" |
|||
"github.com/h2non/filetype" |
|||
"io" |
|||
) |
|||
|
|||
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 |
|||
} |
|||
|
|||
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 := auth.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 := auth.TokenFromContext(ctx) |
|||
|
|||
if filter.Public != nil { |
|||
if *filter.Public == false { |
|||
if filter.Author == nil || *filter.Author == "" { |
|||
return nil, ErrPrivateNoAuthor |
|||
} |
|||
|
|||
if !token.PermittedUser(*filter.Author, "member", "file.list") { |
|||
return nil, auth.ErrUnauthorized |
|||
} |
|||
} |
|||
} |
|||
|
|||
return s.files.List(ctx, filter) |
|||
} |
|||
|
|||
func (s *FileService) Upload(ctx context.Context, reader io.Reader, name string, size int64) (*models.File, error) { |
|||
if name == "" { |
|||
return nil, ErrInvalidName |
|||
} else if size < 320 || size > 16777216 { |
|||
return nil, ErrInvalidFileSize |
|||
} |
|||
|
|||
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 |
|||
} |
|||
|
|||
panic("implement rest of me") |
|||
} |
|||
|
|||
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 = auth.CheckPermission(ctx, "edit", file) |
|||
if err != nil { |
|||
if !file.Public { |
|||
return nil, repositories.ErrNotFound |
|||
} |
|||
|
|||
return nil, err |
|||
} |
|||
|
|||
return s.files.Update(ctx, *file, models.FileUpdate{ |
|||
Name: name, |
|||
Public: public, |
|||
}) |
|||
} |
|||
|
|||
// 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(p), 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, |
|||
} |
@ -0,0 +1,93 @@ |
|||
package space |
|||
|
|||
import ( |
|||
"context" |
|||
"errors" |
|||
"fmt" |
|||
"git.aiterp.net/rpdata/api/internal/config" |
|||
"io" |
|||
|
|||
minio "github.com/minio/minio-go" |
|||
) |
|||
|
|||
type Client struct { |
|||
bucket string |
|||
urlRoot string |
|||
spaceRoot string |
|||
maxSize int64 |
|||
|
|||
s3 *minio.Client |
|||
} |
|||
|
|||
func (client *Client) MaxSize() int64 { |
|||
return client.maxSize |
|||
} |
|||
|
|||
// UploadFile uploads the file to the space. This does not do any checks on it, so the endpoints should
|
|||
// ensure that's all okay.
|
|||
func (client *Client) UploadFile(ctx context.Context, folder string, name string, mimeType string, reader io.Reader, size int64) (string, error) { |
|||
path := folder + "/" + name |
|||
|
|||
if size > client.maxSize { |
|||
return "", errors.New("file is too big") |
|||
} |
|||
|
|||
_, err := client.s3.PutObjectWithContext(ctx, client.bucket, client.spaceRoot+"/"+path, reader, size, minio.PutObjectOptions{ |
|||
ContentType: mimeType, |
|||
UserMetadata: map[string]string{ |
|||
"x-amz-acl": "public-read", |
|||
}, |
|||
}) |
|||
if err != nil { |
|||
return "", err |
|||
} |
|||
|
|||
_, err = client.s3.StatObject(client.bucket, client.spaceRoot+"/"+path, minio.StatObjectOptions{}) |
|||
if err != nil { |
|||
return "", err |
|||
} |
|||
|
|||
return path, nil |
|||
} |
|||
|
|||
// RemoveFile removes a file from the space
|
|||
func (client *Client) RemoveFile(folder string, name string) error { |
|||
path := folder + "/" + name |
|||
|
|||
return client.s3.RemoveObject(client.bucket, client.spaceRoot+"/"+path) |
|||
} |
|||
|
|||
// DownloadFile opens a file for download, using the same path format as the UploadFile function. Remember to Close it!
|
|||
// The returned stream also has `ReaderAt` and `Seeker`, but those are impl. details.
|
|||
func (client *Client) DownloadFile(ctx context.Context, path string) (io.ReadCloser, error) { |
|||
return client.s3.GetObjectWithContext(ctx, client.bucket, client.spaceRoot+"/"+path, minio.GetObjectOptions{}) |
|||
} |
|||
|
|||
// URLFromPath gets the URL from the path returned by UploadFile
|
|||
func (client *Client) URLFromPath(path string) string { |
|||
return client.urlRoot + path |
|||
} |
|||
|
|||
// Connect connects to a S3 space.
|
|||
func Connect(cfg config.Space) (*Client, error) { |
|||
client, err := minio.New(cfg.Host, cfg.AccessKey, cfg.SecretKey, true) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
exists, err := client.BucketExists(cfg.Bucket) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
if !exists { |
|||
return nil, errors.New("bucket not found") |
|||
} |
|||
|
|||
return &Client{ |
|||
bucket: cfg.Bucket, |
|||
urlRoot: fmt.Sprintf("https://%s.%s/%s/", cfg.Bucket, cfg.Host, cfg.Root), |
|||
spaceRoot: cfg.Root, |
|||
maxSize: cfg.MaxSize, |
|||
s3: client, |
|||
}, nil |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue