diff --git a/.gitignore b/.gitignore index da28697b..2439f292 100644 --- a/.gitignore +++ b/.gitignore @@ -22,5 +22,6 @@ tmp/ tmpFiles/ *.tmp logs/ +files/ lastupdate.tmp commentsRouter*.go diff --git a/controllers/account.go b/controllers/account.go index bdc0ff9e..f7be221d 100644 --- a/controllers/account.go +++ b/controllers/account.go @@ -256,13 +256,13 @@ func (c *ApiController) UploadAvatar() { } dist, _ := base64.StdEncoding.DecodeString(avatarBase64[index+1:]) - err := object.UploadAvatar(provider, user.GetId(), dist) + fileUrl, err := object.UploadAvatar(provider, user.GetId(), dist) if err != nil { c.ResponseError(err.Error()) return } - user.Avatar = fmt.Sprintf("%s/%s.png?time=%s", util.UrlJoin(provider.Domain, "/avatar"), user.GetId(), util.GetCurrentUnixTime()) + user.Avatar = fileUrl object.UpdateUser(user.GetId(), user) resp = Response{Status: "ok", Msg: ""} diff --git a/main.go b/main.go index f1996b37..1c366d87 100644 --- a/main.go +++ b/main.go @@ -45,6 +45,7 @@ func main() { beego.SetStaticPath("/static", "web/build/static") beego.BConfig.WebConfig.DirectoryIndex = true beego.SetStaticPath("/swagger", "swagger") + beego.SetStaticPath("/files", "files") // https://studygolang.com/articles/2303 beego.InsertFilter("*", beego.BeforeRouter, routers.StaticFilter) beego.InsertFilter("*", beego.BeforeRouter, routers.AutoSigninFilter) diff --git a/object/storage.go b/object/storage.go index ea6abc12..b2c9ce3f 100644 --- a/object/storage.go +++ b/object/storage.go @@ -17,14 +17,16 @@ package object import ( "bytes" "fmt" + "strings" + "github.com/casbin/casdoor/storage" "github.com/casbin/casdoor/util" ) -func UploadAvatar(provider *Provider, username string, avatar []byte) error { +func UploadAvatar(provider *Provider, username string, avatar []byte) (string, error) { storageProvider := storage.GetStorageProvider(provider.Type, provider.ClientId, provider.ClientSecret, provider.RegionId, provider.Bucket, provider.Endpoint) if storageProvider == nil { - return fmt.Errorf("the provider type: %s is not supported", provider.Type) + return "", fmt.Errorf("the provider type: %s is not supported", provider.Type) } if provider.Domain == "" { @@ -34,5 +36,22 @@ func UploadAvatar(provider *Provider, username string, avatar []byte) error { path := fmt.Sprintf("%s/%s.png", util.UrlJoin(util.GetUrlPath(provider.Domain), "/avatar"), username) _, err := storageProvider.Put(path, bytes.NewReader(avatar)) - return err + if err != nil { + return "", err + } + + host := "" + if provider.Type != "Local File System" { + // provider.Domain = "https://cdn.casbin.com/casdoor/" + host = util.GetUrlHost(provider.Domain) + if !strings.HasPrefix(host, "http://") && !strings.HasPrefix(host, "https://") { + host = fmt.Sprintf("https://%s", host) + } + } else { + // provider.Domain = "http://localhost:8000" or "https://door.casbin.com" + host = util.UrlJoin(provider.Domain, "/files") + } + + fileUrl := fmt.Sprintf("%s?time=%s", util.UrlJoin(host, path), util.GetCurrentUnixTime()) + return fileUrl, nil } diff --git a/storage/local_file_system.go b/storage/local_file_system.go new file mode 100644 index 00000000..4d32bf2b --- /dev/null +++ b/storage/local_file_system.go @@ -0,0 +1,129 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package storage + +import ( + "io" + "os" + "path/filepath" + "strings" + + "github.com/qor/oss" +) + +var baseFolder = "files" + +// FileSystem file system storage +type FileSystem struct { + Base string +} + +// NewFileSystem initialize the local file system storage +func NewFileSystem(base string) *FileSystem { + absBase, err := filepath.Abs(base) + if err != nil { + panic("local file system storage's base folder is not initialized") + } + + return &FileSystem{Base: absBase} +} + +// GetFullPath get full path from absolute/relative path +func (fileSystem FileSystem) GetFullPath(path string) string { + fullPath := path + if !strings.HasPrefix(path, fileSystem.Base) { + fullPath, _ = filepath.Abs(filepath.Join(fileSystem.Base, path)) + } + return fullPath +} + +// Get receive file with given path +func (fileSystem FileSystem) Get(path string) (*os.File, error) { + return os.Open(fileSystem.GetFullPath(path)) +} + +// GetStream get file as stream +func (fileSystem FileSystem) GetStream(path string) (io.ReadCloser, error) { + return os.Open(fileSystem.GetFullPath(path)) +} + +// Put store a reader into given path +func (fileSystem FileSystem) Put(path string, reader io.Reader) (*oss.Object, error) { + var ( + fullPath = fileSystem.GetFullPath(path) + err = os.MkdirAll(filepath.Dir(fullPath), os.ModePerm) + ) + + if err != nil { + return nil, err + } + + dst, err := os.Create(fullPath) + + if err == nil { + if seeker, ok := reader.(io.ReadSeeker); ok { + seeker.Seek(0, 0) + } + _, err = io.Copy(dst, reader) + } + + return &oss.Object{Path: path, Name: filepath.Base(path), StorageInterface: fileSystem}, err +} + +// Delete delete file +func (fileSystem FileSystem) Delete(path string) error { + return os.Remove(fileSystem.GetFullPath(path)) +} + +// List list all objects under current path +func (fileSystem FileSystem) List(path string) ([]*oss.Object, error) { + var ( + objects []*oss.Object + fullPath = fileSystem.GetFullPath(path) + ) + + filepath.Walk(fullPath, func(path string, info os.FileInfo, err error) error { + if path == fullPath { + return nil + } + + if err == nil && !info.IsDir() { + modTime := info.ModTime() + objects = append(objects, &oss.Object{ + Path: strings.TrimPrefix(path, fileSystem.Base), + Name: info.Name(), + LastModified: &modTime, + StorageInterface: fileSystem, + }) + } + return nil + }) + + return objects, nil +} + +// GetEndpoint get endpoint, FileSystem's endpoint is / +func (fileSystem FileSystem) GetEndpoint() string { + return "/" +} + +// GetURL get public accessible URL +func (fileSystem FileSystem) GetURL(path string) (url string, err error) { + return path, nil +} + +func NewLocalFileSystemStorageProvider(clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface { + return NewFileSystem(baseFolder) +} diff --git a/storage/storage.go b/storage/storage.go index 36a5cb3a..be022a84 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -18,6 +18,8 @@ import "github.com/qor/oss" func GetStorageProvider(providerType string, clientId string, clientSecret string, region string, bucket string, endpoint string) oss.StorageInterface { switch providerType { + case "Local File System": + return NewLocalFileSystemStorageProvider(clientId, clientSecret, region, bucket, endpoint) case "AWS S3": return NewAwsS3StorageProvider(clientId, clientSecret, region, bucket, endpoint) case "Aliyun OSS": diff --git a/util/path.go b/util/path.go index 848d5136..25556777 100644 --- a/util/path.go +++ b/util/path.go @@ -29,10 +29,6 @@ func FileExist(path string) bool { } func UrlJoin(base string, path string) string { - if !strings.HasPrefix(base, "http://") && !strings.HasPrefix(base, "https://") { - base = fmt.Sprintf("https://%s", base) - } - res := fmt.Sprintf("%s/%s", strings.TrimRight(base, "/"), strings.TrimLeft(path, "/")) return res } @@ -41,3 +37,8 @@ func GetUrlPath(urlString string) string { u, _ := url.Parse(urlString) return u.Path } + +func GetUrlHost(urlString string) string { + u, _ := url.Parse(urlString) + return fmt.Sprintf("%s://%s", u.Scheme, u.Host) +} diff --git a/web/src/ProviderEditPage.js b/web/src/ProviderEditPage.js index 261fefe7..431bfbaf 100644 --- a/web/src/ProviderEditPage.js +++ b/web/src/ProviderEditPage.js @@ -148,7 +148,8 @@ class ProviderEditPage extends React.Component { } else if (value === "SMS") { this.updateProviderField('type', 'Aliyun SMS'); } else if (value === "Storage") { - this.updateProviderField('type', 'Aliyun OSS'); + this.updateProviderField('type', 'Local File System'); + this.updateProviderField('domain', Setting.getFullServerUrl()); } })}> { @@ -167,7 +168,12 @@ class ProviderEditPage extends React.Component { {Setting.getLabel(i18next.t("provider:Type"), i18next.t("provider:Type - Tooltip"))} :