Gisle Aune
5 years ago
9 changed files with 436 additions and 37 deletions
-
3go.mod
-
8go.sum
-
104graph/resolvers/mutation.resolvers.go
-
7graph/schema/item.gql
-
22internal/slerrors/forbidden.go
-
130internal/space/space.go
-
109main.go
-
11services/bundle.go
-
53services/upload.go
@ -0,0 +1,22 @@ |
|||
package slerrors |
|||
|
|||
type forbiddenError struct { |
|||
Message string |
|||
} |
|||
|
|||
func (e *forbiddenError) Error() string { |
|||
return "forbidden: " + e.Message |
|||
} |
|||
|
|||
func Forbidden(message string) error { |
|||
return &forbiddenError{Message: message} |
|||
} |
|||
|
|||
func IsForbidden(err error) bool { |
|||
if err == nil { |
|||
return false |
|||
} |
|||
|
|||
_, ok := err.(*forbiddenError) |
|||
return ok |
|||
} |
@ -0,0 +1,130 @@ |
|||
package space |
|||
|
|||
import ( |
|||
"context" |
|||
"errors" |
|||
"fmt" |
|||
"io" |
|||
"strings" |
|||
|
|||
"github.com/minio/minio-go" |
|||
) |
|||
|
|||
var ErrBucketNotFound = errors.New("bucket not found") |
|||
var ErrFileTooBig = errors.New("file is too big") |
|||
var ErrNotEnabled = errors.New("s3 connection not enabled") |
|||
var ErrDifferentURLRoot = errors.New("url root in path is different from current") |
|||
|
|||
type Space struct { |
|||
bucket string |
|||
urlRoot string |
|||
root string |
|||
client *minio.Client |
|||
maxSize int64 |
|||
} |
|||
|
|||
func Connect(host, accessKey, secretKey, bucket string, secure bool, maxSize int64, rootDirectory, urlRoot string) (*Space, error) { |
|||
client, err := minio.New(host, accessKey, secretKey, secure) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
exists, err := client.BucketExists(bucket) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
if !exists { |
|||
return nil, ErrBucketNotFound |
|||
} |
|||
|
|||
if urlRoot == "" { |
|||
urlRoot = fmt.Sprintf("https://%s.%s/%s/", bucket, host, rootDirectory) |
|||
} else if !strings.HasSuffix(urlRoot, "/") { |
|||
urlRoot += "/" |
|||
} |
|||
|
|||
space := &Space{ |
|||
client: client, |
|||
bucket: bucket, |
|||
urlRoot: urlRoot, |
|||
maxSize: maxSize, |
|||
root: rootDirectory, |
|||
} |
|||
|
|||
return space, nil |
|||
} |
|||
|
|||
// 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 (space *Space) UploadFile(ctx context.Context, name string, mimeType string, reader io.Reader, size int64) error { |
|||
if space == nil { |
|||
return ErrNotEnabled |
|||
} |
|||
|
|||
if size > space.maxSize { |
|||
return ErrFileTooBig |
|||
} |
|||
|
|||
_, err := space.client.PutObjectWithContext(ctx, space.bucket, space.root+"/"+name, reader, size, minio.PutObjectOptions{ |
|||
ContentType: mimeType, |
|||
UserMetadata: map[string]string{ |
|||
"x-amz-acl": "public-read", |
|||
}, |
|||
}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
_, err = space.client.StatObject(space.bucket, space.root+"/"+name, minio.StatObjectOptions{}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// RemoveFile removes a file from the space
|
|||
func (space *Space) RemoveFile(ctx context.Context, name string) error { |
|||
if space == nil { |
|||
return ErrNotEnabled |
|||
} |
|||
|
|||
ch := make(chan string, 2) |
|||
ch <- space.root + "/" + name |
|||
close(ch) |
|||
|
|||
err := <-space.client.RemoveObjectsWithContext(ctx, space.bucket, ch) |
|||
|
|||
return err.Err |
|||
} |
|||
|
|||
// DownloadFile opens a file for download, using the same path format as the UploadFile function. Remember to Close it!
|
|||
func (space *Space) DownloadFile(ctx context.Context, path string) (io.ReadCloser, error) { |
|||
if space == nil { |
|||
return nil, ErrNotEnabled |
|||
} |
|||
|
|||
return space.client.GetObjectWithContext(ctx, space.bucket, space.root+"/"+path, minio.GetObjectOptions{}) |
|||
} |
|||
|
|||
// URLFromPath gets the URL from the path returned by UploadFile
|
|||
func (space *Space) URLFromPath(path string) string { |
|||
if space == nil { |
|||
return "" |
|||
} |
|||
|
|||
return space.urlRoot + path |
|||
} |
|||
|
|||
// URLFromPath gets the URL from the path returned by UploadFile
|
|||
func (space *Space) PathFromURL(url string) (string, error) { |
|||
if space == nil { |
|||
return "", ErrNotEnabled |
|||
} |
|||
|
|||
if !strings.HasPrefix(url, space.urlRoot) { |
|||
return "", ErrDifferentURLRoot |
|||
} |
|||
|
|||
return url[len(space.urlRoot):], nil |
|||
} |
@ -1,19 +1,26 @@ |
|||
package services |
|||
|
|||
import "git.aiterp.net/stufflog/server/database" |
|||
import ( |
|||
"git.aiterp.net/stufflog/server/database" |
|||
"git.aiterp.net/stufflog/server/internal/space" |
|||
) |
|||
|
|||
type Bundle struct { |
|||
Auth *Auth |
|||
Upload *Upload |
|||
} |
|||
|
|||
func NewBundle(db database.Database) Bundle { |
|||
func NewBundle(db database.Database, s3 *space.Space) Bundle { |
|||
auth := &Auth{ |
|||
users: db.Users(), |
|||
session: db.Session(), |
|||
projects: db.Projects(), |
|||
} |
|||
|
|||
upload := &Upload{s3: s3} |
|||
|
|||
return Bundle{ |
|||
Auth: auth, |
|||
Upload: upload, |
|||
} |
|||
} |
@ -0,0 +1,53 @@ |
|||
package services |
|||
|
|||
import ( |
|||
"context" |
|||
"git.aiterp.net/stufflog/server/internal/slerrors" |
|||
"git.aiterp.net/stufflog/server/internal/space" |
|||
"io" |
|||
"strings" |
|||
) |
|||
|
|||
type Upload struct { |
|||
s3 *space.Space |
|||
} |
|||
|
|||
func (upload *Upload) UploadImage(ctx context.Context, name string, contentType string, size int64, reader io.Reader) (string, error) { |
|||
if !allowedImages[contentType] { |
|||
return "", slerrors.Forbidden("Content type " + contentType + " is not allowed as an image.") |
|||
} |
|||
if !strings.HasSuffix(name, extensions[contentType]) { |
|||
name += extensions[contentType] |
|||
} |
|||
|
|||
err := upload.s3.UploadFile(ctx, name, contentType, reader, size) |
|||
if err != nil { |
|||
return "", err |
|||
} |
|||
|
|||
return upload.s3.URLFromPath(name), nil |
|||
} |
|||
|
|||
func (upload *Upload) Delete(ctx context.Context, url string) error { |
|||
name, err := upload.s3.PathFromURL(url) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
return upload.s3.RemoveFile(ctx, name) |
|||
} |
|||
|
|||
var allowedImages = map[string]bool{ |
|||
"image/jpg": true, |
|||
"image/jpeg": true, |
|||
"image/gif": true, |
|||
"image/png": true, |
|||
"image/bmp": false, |
|||
} |
|||
|
|||
var extensions = map[string]string{ |
|||
"image/jpg": ".jpg", |
|||
"image/jpeg": ".jpg", |
|||
"image/gif": ".gif", |
|||
"image/png": ".png", |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue