Compare commits
6 Commits
32f0d84427
...
3e90199d98
Author | SHA1 | Date |
---|---|---|
Martin | 3e90199d98 | |
Martin | 81bfb1154d | |
Lennart | 6db99dd2bb | |
Lennart | b8acc5f8af | |
Lennart | 967b32fa31 | |
Lennart | b5a323aeb4 |
|
@ -0,0 +1,57 @@
|
||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"dev.sum7.eu/genofire/golang-lib/web/file/fs"
|
||||||
|
"dev.sum7.eu/genofire/golang-lib/web/file/s3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fsType represents a type of file store.
|
||||||
|
type fsType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
typeFS fsType = iota
|
||||||
|
typeS3
|
||||||
|
)
|
||||||
|
|
||||||
|
var stringToType = map[string]fsType{
|
||||||
|
"fs": typeFS,
|
||||||
|
"s3": typeS3,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *fsType) UnmarshalText(input []byte) error {
|
||||||
|
val, ok := stringToType[string(input)]
|
||||||
|
if !ok {
|
||||||
|
return ErrInvalidFSType
|
||||||
|
}
|
||||||
|
*t = val
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FSInfo is a TOML structure storing access information about a file store.
|
||||||
|
type FSInfo struct {
|
||||||
|
FSType fsType `toml:"type"`
|
||||||
|
// file system
|
||||||
|
Root string `toml:",omitempty"`
|
||||||
|
// s3
|
||||||
|
Endpoint string `toml:",omitempty"`
|
||||||
|
Secure bool `toml:",omitempty"`
|
||||||
|
ID string `toml:",omitempty"`
|
||||||
|
Secret string `toml:",omitempty"`
|
||||||
|
Bucket string `toml:",omitempty"`
|
||||||
|
Location string `toml:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create creates a file store from the information provided.
|
||||||
|
func (i *FSInfo) Create() (FS, error) {
|
||||||
|
switch i.FSType {
|
||||||
|
case typeFS:
|
||||||
|
if len(i.Root) == 0 {
|
||||||
|
return nil, ErrNoFSRoot
|
||||||
|
}
|
||||||
|
return &fs.FS{Root: i.Root}, nil
|
||||||
|
case typeS3:
|
||||||
|
return s3.New(i.Endpoint, i.Secure, i.ID, i.Secret, i.Bucket, i.Location)
|
||||||
|
default:
|
||||||
|
return nil, ErrNotImplementedFSType
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package file_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
fsfile "dev.sum7.eu/genofire/golang-lib/file"
|
||||||
|
"dev.sum7.eu/genofire/golang-lib/web/file"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateFSOK(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
config := file.FSInfo{}
|
||||||
|
err := fsfile.ReadTOML("testdata/createfs_fs.toml", &config)
|
||||||
|
assert.NoError(err)
|
||||||
|
|
||||||
|
fs, err := config.Create()
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.NoError(fs.Check())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateS3(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
config := file.FSInfo{}
|
||||||
|
err := fsfile.ReadTOML("testdata/createfs_s3.toml", &config)
|
||||||
|
assert.NoError(err)
|
||||||
|
|
||||||
|
fs, err := config.Create()
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.NoError(fs.Check())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateFSNotOK(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
config := file.FSInfo{}
|
||||||
|
err := fsfile.ReadTOML("testdata/createfs_fsnone.toml", &config)
|
||||||
|
assert.NoError(err)
|
||||||
|
|
||||||
|
_, err = config.Create()
|
||||||
|
assert.ErrorIs(err, file.ErrNoFSRoot)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateFSNone(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
config := file.FSInfo{}
|
||||||
|
err := fsfile.ReadTOML("testdata/createfs_none.toml", &config)
|
||||||
|
|
||||||
|
// https://github.com/naoina/toml/pull/51
|
||||||
|
assert.Contains(err.Error(), file.ErrInvalidFSType.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateFSInvalid(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
config := file.FSInfo{}
|
||||||
|
_, err := config.Create()
|
||||||
|
assert.ErrorIs(err, file.ErrNoFSRoot)
|
||||||
|
}
|
|
@ -2,7 +2,9 @@ package file
|
||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
||||||
// Error Messages
|
// errors
|
||||||
var (
|
var (
|
||||||
ErrUnsupportedStorageType = errors.New("storage type invalid")
|
ErrInvalidFSType = errors.New("invalid file store type")
|
||||||
|
ErrNoFSRoot = errors.New("no file store root")
|
||||||
|
ErrNotImplementedFSType = errors.New("FSInfo.Create not implemented for provided file store type")
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
package file
|
|
||||||
|
|
||||||
import "github.com/google/uuid"
|
|
||||||
|
|
||||||
// File to store information in database
|
|
||||||
type File struct {
|
|
||||||
ID uuid.UUID `json:"id" gorm:"type:uuid;default:gen_random_uuid()" example:"32466d63-efa4-4f27-9f2b-a1f06c8e2e1d"`
|
|
||||||
StorageType string `json:"storage_type,omitempty"`
|
|
||||||
Path string `json:"path,omitempty"`
|
|
||||||
Filename string `json:"filename"`
|
|
||||||
ContentType string `json:"content-type"`
|
|
||||||
}
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileInfo interface {
|
||||||
|
fs.FileInfo
|
||||||
|
ID() uuid.UUID
|
||||||
|
ContentType() string
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
Package file abstracts non-hierarchical file stores. Each file consists of a
|
||||||
|
name, a MIME type, a UUID, and data. File names may be duplicate.
|
||||||
|
|
||||||
|
TODO: think about name vs. UUID again—should the ``name'' really be the filename
|
||||||
|
and not the UUID?
|
||||||
|
*/
|
||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// An FS is a file store.
|
||||||
|
type FS interface {
|
||||||
|
// Store stores a new file with the given UUID, name, and MIME type.
|
||||||
|
// Its data is taken from the provided Reader. If it encounters an
|
||||||
|
// error, it does nothing. Any existing file with the same UUID is
|
||||||
|
// overwritten.
|
||||||
|
Store(id uuid.UUID, name, contentType string, data io.Reader) error
|
||||||
|
// RemoveUUID deletes a file.
|
||||||
|
RemoveUUID(id uuid.UUID) error
|
||||||
|
// Open opens a file by its name. If multiple files have the same name,
|
||||||
|
// it is unspecified which one is opened. This may very well be very
|
||||||
|
// slow. This is bad. Go away.
|
||||||
|
Open(name string) (fs.File, error)
|
||||||
|
// OpenUUID opens a file by its UUID.
|
||||||
|
OpenUUID(id uuid.UUID) (fs.File, error)
|
||||||
|
// Check checks the health of the file store. If the file store is not
|
||||||
|
// healthy, it returns a descriptive error. Otherwise, the file store
|
||||||
|
// should be usable.
|
||||||
|
Check() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreFromHTTP stores the first file with given form key from an HTTP
|
||||||
|
// multipart/form-data request. Its Content-Type header is ignored; the type is
|
||||||
|
// detected. The file name is the last part of the provided file name not
|
||||||
|
// containing any slashes or backslashes.
|
||||||
|
//
|
||||||
|
// TODO: store all files with the given key instead of just the first one
|
||||||
|
func StoreFromHTTP(fs FS, r *http.Request, key string) error {
|
||||||
|
file, fileHeader, err := r.FormFile(key)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get file from request: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
buf := make([]byte, 512)
|
||||||
|
n, err := file.Read(buf)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return fmt.Errorf("read from file: %w", err)
|
||||||
|
}
|
||||||
|
contentType := http.DetectContentType(buf[:n])
|
||||||
|
_, err = file.Seek(0, io.SeekStart)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("seek in file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
i := strings.LastIndexAny(fileHeader.Filename, "/\\")
|
||||||
|
// if i == -1 { i = -1 }
|
||||||
|
|
||||||
|
return fs.Store(uuid.New(), fileHeader.Filename[i+1:], contentType, file)
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package fs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// File is a file handle with its associated ID.
|
||||||
|
type File struct {
|
||||||
|
*os.File
|
||||||
|
fs *FS
|
||||||
|
id uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) Stat() (fs.FileInfo, error) {
|
||||||
|
fi := FileInfo{id: f.id}
|
||||||
|
var err error
|
||||||
|
fi.FileInfo, err = f.File.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("os stat: %w", err)
|
||||||
|
}
|
||||||
|
name, err := os.ReadFile(path.Join(f.fs.Root, f.id.String(), "name"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading name: %w", err)
|
||||||
|
}
|
||||||
|
fi.name = string(name)
|
||||||
|
contentType, err := os.ReadFile(path.Join(f.fs.Root, f.id.String(), "type"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("reading type: %w", err)
|
||||||
|
}
|
||||||
|
fi.contentType = string(contentType)
|
||||||
|
return fi, nil
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package fs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileInfo struct {
|
||||||
|
id uuid.UUID
|
||||||
|
name string
|
||||||
|
contentType string
|
||||||
|
os.FileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fi FileInfo) ID() uuid.UUID { return fi.id }
|
||||||
|
func (fi FileInfo) ContentType() string { return fi.contentType }
|
||||||
|
func (fi FileInfo) Name() string { return fi.name }
|
||||||
|
func (fi FileInfo) Sys() interface{} { return fi.FileInfo }
|
|
@ -0,0 +1,123 @@
|
||||||
|
/*
|
||||||
|
Package fs implements a non-hierarchical file store using the underlying (disk)
|
||||||
|
file system.
|
||||||
|
|
||||||
|
A file store contains directories named after UUIDs, each containing the
|
||||||
|
following files:
|
||||||
|
name the file name (``basename'')
|
||||||
|
type the file's MIME type
|
||||||
|
data the file data
|
||||||
|
|
||||||
|
This is not meant for anything for which the word ``scale'' plays any role at
|
||||||
|
all, ever, anywhere.
|
||||||
|
|
||||||
|
TODO: should io/fs ever support writable file systems, use one of those instead
|
||||||
|
of the ``disk'' (os.Open & Co.) (see https://github.com/golang/go/issues/45757)
|
||||||
|
*/
|
||||||
|
package fs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FS is an on-disk file store.
|
||||||
|
type FS struct {
|
||||||
|
// Root is the path of the file store, relative to the process's working
|
||||||
|
// directory or absolute.
|
||||||
|
Root string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FS) Store(id uuid.UUID, name, contentType string, data io.Reader) error {
|
||||||
|
p := path.Join(fs.Root, id.String())
|
||||||
|
|
||||||
|
_, err := os.Stat(p)
|
||||||
|
if err == nil {
|
||||||
|
err = os.RemoveAll(p)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("remove old %v: %w", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Mkdir(p, 0o750)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("mkdir %s: %w", id.String(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
os.RemoveAll(p)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = os.WriteFile(path.Join(p, "name"), []byte(name), 0o640)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write name: %w", err)
|
||||||
|
}
|
||||||
|
err = os.WriteFile(path.Join(p, "type"), []byte(contentType), 0o640)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write type: %w", err)
|
||||||
|
}
|
||||||
|
f, err := os.Create(path.Join(p, "data"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create data file: %w", err)
|
||||||
|
}
|
||||||
|
_, err = io.Copy(f, data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write data: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FS) RemoveUUID(id uuid.UUID) error {
|
||||||
|
return os.RemoveAll(path.Join(fs.Root, id.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenUUID opens the file with the given UUID.
|
||||||
|
func (fs *FS) OpenUUID(id uuid.UUID) (fs.File, error) {
|
||||||
|
f, err := os.Open(path.Join(fs.Root, id.String(), "data"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open data: %w", err)
|
||||||
|
}
|
||||||
|
return File{File: f, fs: fs, id: id}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open searches for and opens the file with the given name.
|
||||||
|
func (fs *FS) Open(name string) (fs.File, error) {
|
||||||
|
files, err := os.ReadDir(fs.Root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, v := range files {
|
||||||
|
id := v.Name()
|
||||||
|
entryName, err := os.ReadFile(path.Join(fs.Root, id, "name"))
|
||||||
|
if err != nil || string(entryName) != name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
f, err := os.Open(path.Join(fs.Root, id, "data"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open data: %w", err)
|
||||||
|
}
|
||||||
|
return File{File: f, fs: fs, id: uuid.MustParse(id)}, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("no such file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check checks whether the file store is operable, ie. whether fs.Root exists
|
||||||
|
// and is a directory.
|
||||||
|
func (fs *FS) Check() error {
|
||||||
|
fi, err := os.Stat(fs.Root)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !fi.IsDir() {
|
||||||
|
return errors.New("file store is not a directory")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package fs_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"dev.sum7.eu/genofire/golang-lib/web/file"
|
||||||
|
"dev.sum7.eu/genofire/golang-lib/web/file/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOpenStat(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
var fs file.FS = &fs.FS{Root: "./testdata"}
|
||||||
|
assert.NoError(fs.Check())
|
||||||
|
|
||||||
|
f, err := fs.Open("glenda")
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.NotNil(f)
|
||||||
|
|
||||||
|
fi, err := f.Stat()
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.NotNil(fi)
|
||||||
|
|
||||||
|
assert.Equal(uuid.MustParse("d2750ced-4bdc-41d0-8c2f-5b9de44b84ef"), fi.(file.FileInfo).ID())
|
||||||
|
assert.Equal("text/plain", fi.(file.FileInfo).ContentType())
|
||||||
|
assert.Equal("glenda", fi.Name())
|
||||||
|
assert.Equal(int64(99), fi.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateOpenUUIDRead(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
var fs file.FS = &fs.FS{Root: "./testdata"}
|
||||||
|
assert.NoError(fs.Check())
|
||||||
|
|
||||||
|
err := fs.Store(uuid.MustParse("f9375ccb-ee09-4ecf-917e-b88725efcb68"), "$name", "text/plain", strings.NewReader("hello, world\n"))
|
||||||
|
assert.NoError(err)
|
||||||
|
|
||||||
|
f, err := fs.OpenUUID(uuid.MustParse("f9375ccb-ee09-4ecf-917e-b88725efcb68"))
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.NotNil(f)
|
||||||
|
|
||||||
|
buf, err := io.ReadAll(f)
|
||||||
|
assert.NoError(err)
|
||||||
|
assert.Equal("hello, world\n", string(buf))
|
||||||
|
|
||||||
|
os.RemoveAll("./testdata/f9375ccb-ee09-4ecf-917e-b88725efcb68")
|
||||||
|
}
|
|
@ -1,63 +0,0 @@
|
||||||
package fs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
|
|
||||||
"dev.sum7.eu/genofire/golang-lib/web/file"
|
|
||||||
)
|
|
||||||
|
|
||||||
// consts for filemanager
|
|
||||||
const (
|
|
||||||
StorageTypeFS = "fs"
|
|
||||||
)
|
|
||||||
|
|
||||||
// error messages
|
|
||||||
var (
|
|
||||||
ErrPathNotExistsOrNoDirectory = errors.New("path invalid: not exists or not an directory")
|
|
||||||
)
|
|
||||||
|
|
||||||
// FileManager to handle data on disk
|
|
||||||
type FileManager struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if filemanager could be used
|
|
||||||
func (m *FileManager) Check(s *file.Service) error {
|
|
||||||
info, err := os.Stat(s.Path)
|
|
||||||
if os.IsNotExist(err) || !info.IsDir() {
|
|
||||||
return ErrPathNotExistsOrNoDirectory
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save a file on disk and update file db
|
|
||||||
func (m *FileManager) Save(s *file.Service, file *file.File, src multipart.File) error {
|
|
||||||
file.ID = uuid.New()
|
|
||||||
file.Path = path.Join(file.ID.String(), file.Filename)
|
|
||||||
|
|
||||||
directory := path.Join(s.Path, file.ID.String())
|
|
||||||
os.Mkdir(directory, 0750)
|
|
||||||
|
|
||||||
out, err := os.Create(path.Join(s.Path, file.Path))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(out, src)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read get an reader of an file
|
|
||||||
func (m *FileManager) Read(s *file.Service, file *file.File) (io.Reader, error) {
|
|
||||||
return os.Open(path.Join(s.Path, file.Path))
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
file.AddManager(StorageTypeFS, &FileManager{})
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
package fs
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
"dev.sum7.eu/genofire/golang-lib/web"
|
|
||||||
"dev.sum7.eu/genofire/golang-lib/web/file"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCheck(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
service := file.Service{
|
|
||||||
StorageType: StorageTypeFS,
|
|
||||||
Path: "./test",
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.NoError(service.Check())
|
|
||||||
|
|
||||||
service.StorageType = "s3"
|
|
||||||
assert.ErrorIs(file.ErrUnsupportedStorageType, service.Check())
|
|
||||||
|
|
||||||
service.StorageType = StorageTypeFS
|
|
||||||
service.Path = "./main_test.go"
|
|
||||||
assert.ErrorIs(ErrPathNotExistsOrNoDirectory, service.Check())
|
|
||||||
|
|
||||||
/* TODO no write permission
|
|
||||||
service.Path = "/dev"
|
|
||||||
assert.ErrorIs(ErrPathNotExistsOrNoDirectory, service.Check())
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSave(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
service := file.Service{
|
|
||||||
StorageType: "s3",
|
|
||||||
Path: "./test",
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := service.Upload(nil)
|
|
||||||
assert.ErrorIs(file.ErrUnsupportedStorageType, err)
|
|
||||||
|
|
||||||
service.StorageType = StorageTypeFS
|
|
||||||
req, err := web.NewRequestWithFile("localhost", "./test/00000000-0000-0000-0000-000000000000/a.txt")
|
|
||||||
assert.NoError(err)
|
|
||||||
assert.NotNil(req)
|
|
||||||
|
|
||||||
_, err = service.Upload(req)
|
|
||||||
assert.NoError(err)
|
|
||||||
|
|
||||||
service.Path = "/dev"
|
|
||||||
_, err = service.Upload(req)
|
|
||||||
assert.True(os.IsNotExist(err))
|
|
||||||
//assert.True(os.IsPermission(err))
|
|
||||||
|
|
||||||
// TODO no write permission
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRead(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
service := file.Service{
|
|
||||||
StorageType: "s3",
|
|
||||||
Path: "./test",
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := service.Read(nil)
|
|
||||||
assert.ErrorIs(file.ErrUnsupportedStorageType, err)
|
|
||||||
|
|
||||||
service.StorageType = StorageTypeFS
|
|
||||||
|
|
||||||
file := &file.File{
|
|
||||||
Path: "00000000-0000-0000-0000-000000000000/a.txt",
|
|
||||||
}
|
|
||||||
r, err := service.Read(file)
|
|
||||||
assert.NoError(err)
|
|
||||||
buf := &strings.Builder{}
|
|
||||||
_, err = io.Copy(buf, r)
|
|
||||||
assert.Equal("Hello world\n", buf.String())
|
|
||||||
|
|
||||||
service.Path = "/dev"
|
|
||||||
_, err = service.Read(file)
|
|
||||||
assert.True(os.IsNotExist(err))
|
|
||||||
|
|
||||||
// TODO no write permission
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
Hello world
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
Small ‘hand-drwawn’ Glenda by ems:
|
||||||
|
|
||||||
|
(\(\
|
||||||
|
¸". ..
|
||||||
|
( . .)
|
||||||
|
| ° ¡
|
||||||
|
¿ ;
|
||||||
|
c?".UJ"
|
|
@ -0,0 +1 @@
|
||||||
|
glenda
|
|
@ -0,0 +1 @@
|
||||||
|
text/plain
|
|
@ -0,0 +1,111 @@
|
||||||
|
package file_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"mime"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"dev.sum7.eu/genofire/golang-lib/web/file"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestFS struct {
|
||||||
|
assert *assert.Assertions
|
||||||
|
filename string
|
||||||
|
data string
|
||||||
|
contentType string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f TestFS) Store(id uuid.UUID, name, contentType string, data io.Reader) error {
|
||||||
|
f.assert.Equal(f.filename, name)
|
||||||
|
dat, err := io.ReadAll(data)
|
||||||
|
f.assert.NoError(err)
|
||||||
|
f.assert.Equal(f.data, string(dat))
|
||||||
|
contentType, _, err = mime.ParseMediaType(contentType)
|
||||||
|
f.assert.NoError(err)
|
||||||
|
f.assert.Equal(f.contentType, contentType)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f TestFS) RemoveUUID(id uuid.UUID) error {
|
||||||
|
return errors.New("TestFS.RemoveUUID called")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f TestFS) Open(name string) (fs.File, error) {
|
||||||
|
return nil, errors.New("TestFS.Open called")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f TestFS) OpenUUID(uuid.UUID) (fs.File, error) {
|
||||||
|
return nil, errors.New("TestFS.OpenUUID called")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f TestFS) Check() error { return nil }
|
||||||
|
|
||||||
|
func TestStoreFromHTTP(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
testfs := TestFS{
|
||||||
|
assert: assert,
|
||||||
|
filename: "cute-cat.png",
|
||||||
|
data: "content\nof file",
|
||||||
|
contentType: "text/plain",
|
||||||
|
}
|
||||||
|
|
||||||
|
r, w := io.Pipe()
|
||||||
|
m := multipart.NewWriter(w)
|
||||||
|
rq := httptest.NewRequest("PUT", "/", r)
|
||||||
|
rq.Header.Set("Content-Type", m.FormDataContentType())
|
||||||
|
go func() {
|
||||||
|
f, err := m.CreateFormFile("file", testfs.filename)
|
||||||
|
assert.NoError(err)
|
||||||
|
_, err = f.Write([]byte(testfs.data))
|
||||||
|
assert.NoError(err)
|
||||||
|
m.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
assert.NoError(file.StoreFromHTTP(testfs, rq, "file"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var fstore file.FS
|
||||||
|
|
||||||
|
func ExampleFS() {
|
||||||
|
// generate the UUID for the new file
|
||||||
|
id := uuid.New()
|
||||||
|
|
||||||
|
// store a file
|
||||||
|
{
|
||||||
|
f, _ := os.Open("glenda.png")
|
||||||
|
fstore.Store(id, "glenda.png", "image/png", f)
|
||||||
|
f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy back to a local file
|
||||||
|
{
|
||||||
|
r, _ := fstore.OpenUUID(id)
|
||||||
|
w, _ := os.Create("glenda.png")
|
||||||
|
io.Copy(w, r)
|
||||||
|
r.Close()
|
||||||
|
w.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleStoreFromHTTP() {
|
||||||
|
http.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := file.StoreFromHTTP(fstore, r, "file"); err != nil {
|
||||||
|
w.Header().Set("content-type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
w.Write([]byte(fmt.Sprintf(`{"message":"%v"}`, err)))
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,98 +0,0 @@
|
||||||
package file
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"dev.sum7.eu/genofire/golang-lib/web"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
const storageTypeDummy = "dummy"
|
|
||||||
|
|
||||||
type dummyManager struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *dummyManager) Check(s *Service) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (m *dummyManager) Save(s *Service, file *File, src multipart.File) error {
|
|
||||||
if src == nil {
|
|
||||||
return errors.New("nothing to fill")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (m *dummyManager) Read(s *Service, file *File) (io.Reader, error) {
|
|
||||||
b := bytes.Buffer{}
|
|
||||||
b.WriteString("Hello world\n")
|
|
||||||
return &b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
AddManager(storageTypeDummy, &dummyManager{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheck(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
service := Service{
|
|
||||||
StorageType: storageTypeDummy,
|
|
||||||
Path: "./fs/test",
|
|
||||||
}
|
|
||||||
assert.NoError(service.Check())
|
|
||||||
|
|
||||||
service.StorageType = "s3"
|
|
||||||
assert.ErrorIs(ErrUnsupportedStorageType, service.Check())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSave(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
service := Service{
|
|
||||||
StorageType: "fs",
|
|
||||||
Path: "./fs/test",
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := service.Upload(nil)
|
|
||||||
assert.ErrorIs(ErrUnsupportedStorageType, err)
|
|
||||||
|
|
||||||
service.StorageType = storageTypeDummy
|
|
||||||
_, err = service.GINUpload(&gin.Context{Request: &http.Request{}})
|
|
||||||
assert.ErrorIs(err, http.ErrNotMultipart)
|
|
||||||
|
|
||||||
req, err := web.NewRequestWithFile("http://localhost/upload", "./fs/test/00000000-0000-0000-0000-000000000000/a.txt")
|
|
||||||
assert.NoError(err)
|
|
||||||
assert.NotNil(req)
|
|
||||||
|
|
||||||
_, err = service.Upload(req)
|
|
||||||
assert.NoError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRead(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
service := Service{
|
|
||||||
StorageType: "fs",
|
|
||||||
Path: "./fs/test",
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := service.Read(nil)
|
|
||||||
assert.ErrorIs(ErrUnsupportedStorageType, err)
|
|
||||||
|
|
||||||
service.StorageType = "dummy"
|
|
||||||
|
|
||||||
file := &File{
|
|
||||||
Path: "00000000-0000-0000-0000-000000000000/a.txt",
|
|
||||||
}
|
|
||||||
r, err := service.Read(file)
|
|
||||||
assert.NoError(err)
|
|
||||||
buf := &strings.Builder{}
|
|
||||||
_, err = io.Copy(buf, r)
|
|
||||||
assert.Equal("Hello world\n", buf.String())
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package file
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FileManager interface {
|
|
||||||
Check(s *Service) error
|
|
||||||
Save(s *Service, fileObj *File, file multipart.File) error
|
|
||||||
Read(s *Service, fileObj *File) (io.Reader, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
managers = make(map[string]FileManager)
|
|
||||||
)
|
|
||||||
|
|
||||||
func AddManager(typ string, m FileManager) {
|
|
||||||
managers[typ] = m
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
package s3
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/minio/minio-go/v7"
|
|
||||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Error Messages during connect
|
|
||||||
var (
|
|
||||||
ErrNoPassword = errors.New("no secret access key found")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Connect try to use a path to setup a connection to s3 server
|
|
||||||
func Connect(path string) (*minio.Client, string, error) {
|
|
||||||
u, err := url.Parse(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
tls := u.Scheme[len(u.Scheme)-1] == 's'
|
|
||||||
accessKeyID := u.User.Username()
|
|
||||||
secretAccessKey, ok := u.User.Password()
|
|
||||||
if !ok {
|
|
||||||
return nil, "", ErrNoPassword
|
|
||||||
}
|
|
||||||
query := u.Query()
|
|
||||||
bucketName := query.Get("bucket")
|
|
||||||
location := query.Get("location")
|
|
||||||
|
|
||||||
u.User = nil
|
|
||||||
u.RawQuery = ""
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Initialize minio client object.
|
|
||||||
minioClient, err := minio.New(u.String(), &minio.Options{
|
|
||||||
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
|
|
||||||
Secure: tls,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// create and check for bucket
|
|
||||||
err = minioClient.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{Region: location})
|
|
||||||
if err != nil {
|
|
||||||
if exists, err := minioClient.BucketExists(ctx, bucketName); err != nil || !exists {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return minioClient, bucketName, err
|
|
||||||
}
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
package s3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
)
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
*minio.Object
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) Stat() (fs.FileInfo, error) {
|
||||||
|
var fi FileInfo
|
||||||
|
var err error
|
||||||
|
fi.ObjectInfo, err = f.Object.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fi, nil
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package s3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileInfo struct {
|
||||||
|
minio.ObjectInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fi FileInfo) Name() string { return fi.UserMetadata["filename"] }
|
||||||
|
func (fi FileInfo) Size() int64 { return fi.ObjectInfo.Size }
|
||||||
|
|
||||||
|
// TODO: try to map s3 permissions to these, somehow
|
||||||
|
func (fi FileInfo) Mode() fs.FileMode { return 0o640 }
|
||||||
|
func (fi FileInfo) ModTime() time.Time { return fi.LastModified }
|
||||||
|
func (fi FileInfo) IsDir() bool { return false }
|
||||||
|
func (fi FileInfo) Sys() interface{} { return fi.ObjectInfo }
|
||||||
|
func (fi FileInfo) ID() uuid.UUID { return uuid.MustParse(fi.Key) }
|
||||||
|
func (fi FileInfo) ContentType() string { return fi.ObjectInfo.ContentType }
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
Package s3 implements a non-hierarchical file store using Amazon s3. A file
|
||||||
|
store uses a single bucket. Each file is an object with its UUID as the name.
|
||||||
|
The file name is stored in the user-defined object metadata x-amz-meta-filename.
|
||||||
|
*/
|
||||||
|
package s3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FS struct {
|
||||||
|
client *minio.Client
|
||||||
|
bucket string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New ``connects'' to an s3 endpoint and creates a file store using the
|
||||||
|
// specified bucket. The bucket is created if it doesn't exist.
|
||||||
|
func New(endpoint string, secure bool, id, secret, bucket, location string) (*FS, error) {
|
||||||
|
var fs FS
|
||||||
|
var err error
|
||||||
|
fs.client, err = minio.New(endpoint, &minio.Options{
|
||||||
|
Creds: credentials.NewStaticV4(id, secret, ""),
|
||||||
|
Secure: secure,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.TODO()
|
||||||
|
|
||||||
|
err = fs.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{
|
||||||
|
Region: location,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if exists, err := fs.client.BucketExists(ctx, bucket); err != nil || !exists {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.bucket = bucket
|
||||||
|
|
||||||
|
return &fs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FS) Store(id uuid.UUID, name, contentType string, data io.Reader) error {
|
||||||
|
ctx := context.TODO()
|
||||||
|
_, err := fs.client.PutObject(ctx, fs.bucket, id.String(), data, -1, minio.PutObjectOptions{
|
||||||
|
UserMetadata: map[string]string{
|
||||||
|
"filename": name,
|
||||||
|
},
|
||||||
|
ContentType: contentType,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FS) RemoveUUID(id uuid.UUID) error {
|
||||||
|
ctx := context.TODO()
|
||||||
|
return fs.client.RemoveObject(ctx, fs.bucket, id.String(), minio.RemoveObjectOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement
|
||||||
|
func (fs *FS) Open(name string) (fs.File, error) {
|
||||||
|
return nil, errors.New("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FS) OpenUUID(id uuid.UUID) (fs.File, error) {
|
||||||
|
ctx := context.TODO()
|
||||||
|
|
||||||
|
object, err := fs.client.GetObject(ctx, fs.bucket, id.String(), minio.GetObjectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return File{Object: object}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: do some checking
|
||||||
|
func (fs *FS) Check() error {
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package s3_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"dev.sum7.eu/genofire/golang-lib/web/file"
|
||||||
|
"dev.sum7.eu/genofire/golang-lib/web/file/s3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: actually test, either using little dummies or using sth like play.min.io
|
||||||
|
|
||||||
|
func TestTypes(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
var fstore file.FS
|
||||||
|
fstore, err := s3.New("127.0.0.1", false, "", "", "", "")
|
||||||
|
_ = fstore
|
||||||
|
assert.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleNew() {
|
||||||
|
s3.New("play.min.io", true, "Q3AM3UQ867SPQQA43P2F", "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG", "file-store", "")
|
||||||
|
}
|
|
@ -1,69 +0,0 @@
|
||||||
package file
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A Service to handle file-uploads in golang
|
|
||||||
type Service struct {
|
|
||||||
StorageType string `toml:"storage_type"`
|
|
||||||
Path string `toml:"path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if Service is configurated and useable
|
|
||||||
func (s *Service) Check() error {
|
|
||||||
mgmt, ok := managers[s.StorageType]
|
|
||||||
if !ok {
|
|
||||||
return ErrUnsupportedStorageType
|
|
||||||
}
|
|
||||||
return mgmt.Check(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload a file to storage
|
|
||||||
func (s *Service) Upload(request *http.Request) (*File, error) {
|
|
||||||
mgmt, ok := managers[s.StorageType]
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrUnsupportedStorageType
|
|
||||||
}
|
|
||||||
file, fileRequest, err := request.FormFile("file")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
fileObj := File{
|
|
||||||
Filename: filepath.Base(fileRequest.Filename),
|
|
||||||
}
|
|
||||||
|
|
||||||
// detect contenttype
|
|
||||||
buffer := make([]byte, 512)
|
|
||||||
n, err := file.Read(buffer)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
fileObj.ContentType = http.DetectContentType(buffer[:n])
|
|
||||||
|
|
||||||
// Reset the read pointer
|
|
||||||
file.Seek(0, io.SeekStart)
|
|
||||||
if err := mgmt.Save(s, &fileObj, file); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &fileObj, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// GINUpload a file to storage using gin-gonic
|
|
||||||
func (s *Service) GINUpload(c *gin.Context) (*File, error) {
|
|
||||||
return s.Upload(c.Request)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read a file to storage
|
|
||||||
func (s *Service) Read(file *File) (io.Reader, error) {
|
|
||||||
mgmt, ok := managers[s.StorageType]
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrUnsupportedStorageType
|
|
||||||
}
|
|
||||||
return mgmt.Read(s, file)
|
|
||||||
}
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
type = "fs"
|
||||||
|
root = "fs/testdata"
|
|
@ -0,0 +1,2 @@
|
||||||
|
type = "fs"
|
||||||
|
root = ""
|
|
@ -0,0 +1 @@
|
||||||
|
type = "notsupported"
|
|
@ -0,0 +1,7 @@
|
||||||
|
type = "s3"
|
||||||
|
endpoint = "play.min.io"
|
||||||
|
secure = true
|
||||||
|
id = "Q3AM3UQ867SPQQA43P2F"
|
||||||
|
secret = "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG"
|
||||||
|
bucket = "file-store"
|
||||||
|
location = ""
|
Loading…
Reference in New Issue