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
-
131main.go
-
15services/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 |
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 { |
type Bundle struct { |
||||
Auth *Auth |
|
||||
|
Auth *Auth |
||||
|
Upload *Upload |
||||
} |
} |
||||
|
|
||||
func NewBundle(db database.Database) Bundle { |
|
||||
|
func NewBundle(db database.Database, s3 *space.Space) Bundle { |
||||
auth := &Auth{ |
auth := &Auth{ |
||||
users: db.Users(), |
users: db.Users(), |
||||
session: db.Session(), |
session: db.Session(), |
||||
projects: db.Projects(), |
projects: db.Projects(), |
||||
} |
} |
||||
|
|
||||
|
upload := &Upload{s3: s3} |
||||
|
|
||||
return Bundle{ |
return Bundle{ |
||||
Auth: auth, |
|
||||
|
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