mirror of https://github.com/usememos/memos
feat: storage service backend (#1086)
* feat: storage service backend * update go.mod * update the column name (urlPrefix -> url_prefix) * update * updatepull/1087/head
parent
cbc3373e8e
commit
1e4a81dea9
@ -0,0 +1,67 @@
|
|||||||
|
package s3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/config"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
|
||||||
|
awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||||
|
"github.com/usememos/memos/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
Client *awss3.Client
|
||||||
|
BucketName string
|
||||||
|
URLPrefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(ctx context.Context, storage *api.Storage) (*Client, error) {
|
||||||
|
resolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
|
||||||
|
return aws.Endpoint{
|
||||||
|
URL: storage.EndPoint,
|
||||||
|
SigningRegion: storage.Region,
|
||||||
|
}, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
cfg, err := config.LoadDefaultConfig(ctx,
|
||||||
|
config.WithEndpointResolverWithOptions(resolver),
|
||||||
|
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(storage.AccessKey, storage.SecretKey, "")),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := awss3.NewFromConfig(cfg)
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
Client: client,
|
||||||
|
BucketName: storage.Bucket,
|
||||||
|
URLPrefix: storage.URLPrefix,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) UploadFile(ctx context.Context, filename string, fileType string, src io.Reader, storage *api.Storage) (*string, error) {
|
||||||
|
uploader := manager.NewUploader(client.Client)
|
||||||
|
resp, err := uploader.Upload(ctx, &awss3.PutObjectInput{
|
||||||
|
Bucket: aws.String(client.BucketName),
|
||||||
|
Key: aws.String(filename),
|
||||||
|
Body: src,
|
||||||
|
ContentType: aws.String(fileType),
|
||||||
|
ACL: types.ObjectCannedACL(*aws.String("public-read")),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var link string
|
||||||
|
if storage.URLPrefix == "" {
|
||||||
|
link = resp.Location
|
||||||
|
} else {
|
||||||
|
link = fmt.Sprintf("%s/%s", storage.URLPrefix, filename)
|
||||||
|
}
|
||||||
|
return &link, nil
|
||||||
|
}
|
@ -0,0 +1,143 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/usememos/memos/api"
|
||||||
|
"github.com/usememos/memos/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Server) registerStorageRoutes(g *echo.Group) {
|
||||||
|
g.POST("/storage", func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||||
|
if !ok {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := s.Store.FindUser(ctx, &api.UserFind{
|
||||||
|
ID: &userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
|
||||||
|
}
|
||||||
|
if user == nil || user.Role != api.Host {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
storageCreate := &api.StorageCreate{}
|
||||||
|
if err := json.NewDecoder(c.Request().Body).Decode(storageCreate); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post storage request").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
storageCreate.CreatorID = userID
|
||||||
|
storage, err := s.Store.CreateStorage(ctx, storageCreate)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create storage").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||||
|
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(storage)); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode storage response").SetInternal(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
g.PATCH("/storage/:storageId", func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||||
|
if !ok {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||||
|
}
|
||||||
|
|
||||||
|
storageID, err := strconv.Atoi(c.Param("storageId"))
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("storageId"))).SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
storage, err := s.Store.FindStorage(ctx, &api.StorageFind{
|
||||||
|
ID: &storageID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
|
||||||
|
}
|
||||||
|
if storage.CreatorID != userID {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTs := time.Now().Unix()
|
||||||
|
storagePatch := &api.StoragePatch{
|
||||||
|
ID: storageID,
|
||||||
|
UpdatedTs: ¤tTs,
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(c.Request().Body).Decode(storagePatch); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch storage request").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
storage, err = s.Store.PatchStorage(ctx, storagePatch)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch storage").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||||
|
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(storage)); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode memo response").SetInternal(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
g.GET("/storage", func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
storageList, err := s.Store.FindStorageList(ctx, &api.StorageFind{})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage list").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)
|
||||||
|
if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(storageList)); err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode storage list response").SetInternal(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
g.DELETE("/storage/:storageId", func(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
userID, ok := c.Get(getUserIDContextKey()).(int)
|
||||||
|
if !ok {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
|
||||||
|
}
|
||||||
|
|
||||||
|
storageID, err := strconv.Atoi(c.Param("storageId"))
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("storageId"))).SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
storage, err := s.Store.FindStorage(ctx, &api.StorageFind{
|
||||||
|
ID: &storageID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
|
||||||
|
}
|
||||||
|
if storage.CreatorID != userID {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
storageDelete := &api.StorageDelete{
|
||||||
|
ID: storageID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = s.Store.DeleteStorage(ctx, storageDelete); err != nil {
|
||||||
|
if common.ErrorCode(err) == common.NotFound {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Storage ID not found: %d", storageID))
|
||||||
|
}
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete storage").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, true)
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue