refactor: migrate resource to apiv1 (#1901)

This commit is contained in:
boojack
2023-07-06 00:01:40 +08:00
committed by GitHub
parent 5ea561af3d
commit 1fa9f162a5
15 changed files with 462 additions and 382 deletions

View File

@ -20,51 +20,3 @@ type Resource struct {
// Related fields // Related fields
LinkedMemoAmount int `json:"linkedMemoAmount"` LinkedMemoAmount int `json:"linkedMemoAmount"`
} }
type ResourceCreate struct {
// Standard fields
CreatorID int `json:"-"`
// Domain specific fields
Filename string `json:"filename"`
Blob []byte `json:"-"`
InternalPath string `json:"internalPath"`
ExternalLink string `json:"externalLink"`
Type string `json:"type"`
Size int64 `json:"-"`
PublicID string `json:"publicId"`
DownloadToLocal bool `json:"downloadToLocal"`
}
type ResourceFind struct {
ID *int `json:"id"`
// Standard fields
CreatorID *int `json:"creatorId"`
// Domain specific fields
Filename *string `json:"filename"`
MemoID *int
PublicID *string `json:"publicId"`
GetBlob bool
// Pagination
Limit *int
Offset *int
}
type ResourcePatch struct {
ID int `json:"-"`
// Standard fields
UpdatedTs *int64
// Domain specific fields
Filename *string `json:"filename"`
ResetPublicID *bool `json:"resetPublicId"`
PublicID *string `json:"-"`
}
type ResourceDelete struct {
ID int
}

24
api/v1/memo_resource.go Normal file
View File

@ -0,0 +1,24 @@
package v1
type MemoResource struct {
MemoID int
ResourceID int
CreatedTs int64
UpdatedTs int64
}
type MemoResourceUpsert struct {
MemoID int `json:"-"`
ResourceID int
UpdatedTs *int64
}
type MemoResourceFind struct {
MemoID *int
ResourceID *int
}
type MemoResourceDelete struct {
MemoID *int
ResourceID *int
}

View File

@ -1,4 +1,4 @@
package server package v1
import ( import (
"bytes" "bytes"
@ -21,8 +21,6 @@ import (
"github.com/disintegration/imaging" "github.com/disintegration/imaging"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/usememos/memos/api"
apiv1 "github.com/usememos/memos/api/v1"
"github.com/usememos/memos/common" "github.com/usememos/memos/common"
"github.com/usememos/memos/common/log" "github.com/usememos/memos/common/log"
"github.com/usememos/memos/plugin/storage/s3" "github.com/usememos/memos/plugin/storage/s3"
@ -30,6 +28,48 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
type Resource struct {
ID int `json:"id"`
// Standard fields
CreatorID int `json:"creatorId"`
CreatedTs int64 `json:"createdTs"`
UpdatedTs int64 `json:"updatedTs"`
// Domain specific fields
Filename string `json:"filename"`
Blob []byte `json:"-"`
InternalPath string `json:"-"`
ExternalLink string `json:"externalLink"`
Type string `json:"type"`
Size int64 `json:"size"`
PublicID string `json:"publicId"`
// Related fields
LinkedMemoAmount int `json:"linkedMemoAmount"`
}
type CreateResourceRequest struct {
Filename string `json:"filename"`
InternalPath string `json:"internalPath"`
ExternalLink string `json:"externalLink"`
Type string `json:"type"`
PublicID string `json:"publicId"`
DownloadToLocal bool `json:"downloadToLocal"`
}
type FindResourceRequest struct {
ID *int `json:"id"`
CreatorID *int `json:"creatorId"`
Filename *string `json:"filename"`
PublicID *string `json:"publicId"`
}
type UpdateResourceRequest struct {
Filename *string `json:"filename"`
ResetPublicID *bool `json:"resetPublicId"`
}
const ( const (
// The upload memory buffer is 32 MiB. // The upload memory buffer is 32 MiB.
// It should be kept low, so RAM usage doesn't get out of control. // It should be kept low, so RAM usage doesn't get out of control.
@ -43,7 +83,7 @@ const (
var fileKeyPattern = regexp.MustCompile(`\{[a-z]{1,9}\}`) var fileKeyPattern = regexp.MustCompile(`\{[a-z]{1,9}\}`)
func (s *Server) registerResourceRoutes(g *echo.Group) { func (s *APIV1Service) registerResourceRoutes(g *echo.Group) {
g.POST("/resource", func(c echo.Context) error { g.POST("/resource", func(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()
userID, ok := c.Get(getUserIDContextKey()).(int) userID, ok := c.Get(getUserIDContextKey()).(int)
@ -51,15 +91,21 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session") return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
} }
resourceCreate := &api.ResourceCreate{} request := &CreateResourceRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(resourceCreate); err != nil { if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post resource request").SetInternal(err) return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post resource request").SetInternal(err)
} }
resourceCreate.CreatorID = userID create := &store.Resource{
if resourceCreate.ExternalLink != "" { CreatorID: userID,
Filename: request.Filename,
ExternalLink: request.ExternalLink,
Type: request.Type,
PublicID: common.GenUUID(),
}
if request.ExternalLink != "" {
// Only allow those external links scheme with http/https // Only allow those external links scheme with http/https
linkURL, err := url.Parse(resourceCreate.ExternalLink) linkURL, err := url.Parse(request.ExternalLink)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link").SetInternal(err) return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link").SetInternal(err)
} }
@ -67,24 +113,24 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link scheme") return echo.NewHTTPError(http.StatusBadRequest, "Invalid external link scheme")
} }
if resourceCreate.DownloadToLocal { if request.DownloadToLocal {
resp, err := http.Get(linkURL.String()) resp, err := http.Get(linkURL.String())
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Failed to request "+resourceCreate.ExternalLink) return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Failed to request %s", request.ExternalLink))
} }
defer resp.Body.Close() defer resp.Body.Close()
blob, err := io.ReadAll(resp.Body) blob, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Failed to read "+resourceCreate.ExternalLink) return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Failed to read %s", request.ExternalLink))
} }
resourceCreate.Blob = blob create.Blob = blob
mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Failed to read mime from "+resourceCreate.ExternalLink) return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Failed to read mime from %s", request.ExternalLink))
} }
resourceCreate.Type = mediaType create.Type = mediaType
filename := path.Base(linkURL.Path) filename := path.Base(linkURL.Path)
if path.Ext(filename) == "" { if path.Ext(filename) == "" {
@ -93,20 +139,19 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
filename += extensions[0] filename += extensions[0]
} }
} }
resourceCreate.Filename = filename create.Filename = filename
resourceCreate.PublicID = common.GenUUID() create.ExternalLink = ""
resourceCreate.ExternalLink = ""
} }
} }
resource, err := s.Store.CreateResource(ctx, resourceCreate) resource, err := s.Store.CreateResourceV1(ctx, create)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
} }
if err := s.createResourceCreateActivity(ctx, resource); err != nil { if err := s.createResourceCreateActivity(ctx, resource); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
} }
return c.JSON(http.StatusOK, composeResponse(resource)) return c.JSON(http.StatusOK, convertResourceFromStore(resource))
}) })
g.POST("/resource/blob", func(c echo.Context) error { g.POST("/resource/blob", func(c echo.Context) error {
@ -117,7 +162,7 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
} }
// This is the backend default max upload size limit. // This is the backend default max upload size limit.
maxUploadSetting := s.Store.GetSystemSettingValueWithDefault(&ctx, apiv1.SystemSettingMaxUploadSizeMiBName.String(), "32") maxUploadSetting := s.Store.GetSystemSettingValueWithDefault(&ctx, SystemSettingMaxUploadSizeMiBName.String(), "32")
var settingMaxUploadSizeBytes int var settingMaxUploadSizeBytes int
if settingMaxUploadSizeMiB, err := strconv.Atoi(maxUploadSetting); err == nil { if settingMaxUploadSizeMiB, err := strconv.Atoi(maxUploadSetting); err == nil {
settingMaxUploadSizeBytes = settingMaxUploadSizeMiB * MebiByte settingMaxUploadSizeBytes = settingMaxUploadSizeMiB * MebiByte
@ -150,12 +195,12 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
} }
defer sourceFile.Close() defer sourceFile.Close()
var resourceCreate *api.ResourceCreate create := &store.Resource{}
systemSettingStorageServiceID, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{Name: apiv1.SystemSettingStorageServiceIDName.String()}) systemSettingStorageServiceID, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{Name: SystemSettingStorageServiceIDName.String()})
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
} }
storageServiceID := apiv1.DatabaseStorage storageServiceID := DatabaseStorage
if systemSettingStorageServiceID != nil { if systemSettingStorageServiceID != nil {
err = json.Unmarshal([]byte(systemSettingStorageServiceID.Value), &storageServiceID) err = json.Unmarshal([]byte(systemSettingStorageServiceID.Value), &storageServiceID)
if err != nil { if err != nil {
@ -164,23 +209,23 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
} }
publicID := common.GenUUID() publicID := common.GenUUID()
if storageServiceID == apiv1.DatabaseStorage { if storageServiceID == DatabaseStorage {
fileBytes, err := io.ReadAll(sourceFile) fileBytes, err := io.ReadAll(sourceFile)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read file").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to read file").SetInternal(err)
} }
resourceCreate = &api.ResourceCreate{ create = &store.Resource{
CreatorID: userID, CreatorID: userID,
Filename: file.Filename, Filename: file.Filename,
Type: filetype, Type: filetype,
Size: size, Size: size,
Blob: fileBytes, Blob: fileBytes,
} }
} else if storageServiceID == apiv1.LocalStorage { } else if storageServiceID == LocalStorage {
// filepath.Join() should be used for local file paths, // filepath.Join() should be used for local file paths,
// as it handles the os-specific path separator automatically. // as it handles the os-specific path separator automatically.
// path.Join() always uses '/' as path separator. // path.Join() always uses '/' as path separator.
systemSettingLocalStoragePath, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{Name: apiv1.SystemSettingLocalStoragePathName.String()}) systemSettingLocalStoragePath, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{Name: SystemSettingLocalStoragePathName.String()})
if err != nil && common.ErrorCode(err) != common.NotFound { if err != nil && common.ErrorCode(err) != common.NotFound {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find local storage path setting").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find local storage path setting").SetInternal(err)
} }
@ -211,7 +256,7 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to copy file").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to copy file").SetInternal(err)
} }
resourceCreate = &api.ResourceCreate{ create = &store.Resource{
CreatorID: userID, CreatorID: userID,
Filename: file.Filename, Filename: file.Filename,
Type: filetype, Type: filetype,
@ -223,12 +268,12 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
} }
storageMessage, err := apiv1.ConvertStorageFromStore(storage) storageMessage, err := ConvertStorageFromStore(storage)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to convert storage").SetInternal(err)
} }
if storageMessage.Type == apiv1.StorageS3 { if storageMessage.Type == StorageS3 {
s3Config := storageMessage.Config.S3Config s3Config := storageMessage.Config.S3Config
s3Client, err := s3.NewClient(ctx, &s3.Config{ s3Client, err := s3.NewClient(ctx, &s3.Config{
AccessKey: s3Config.AccessKey, AccessKey: s3Config.AccessKey,
@ -253,7 +298,7 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upload via s3 client").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upload via s3 client").SetInternal(err)
} }
resourceCreate = &api.ResourceCreate{ create = &store.Resource{
CreatorID: userID, CreatorID: userID,
Filename: filename, Filename: filename,
Type: filetype, Type: filetype,
@ -265,15 +310,15 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
} }
} }
resourceCreate.PublicID = publicID create.PublicID = publicID
resource, err := s.Store.CreateResource(ctx, resourceCreate) resource, err := s.Store.CreateResourceV1(ctx, create)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create resource").SetInternal(err)
} }
if err := s.createResourceCreateActivity(ctx, resource); err != nil { if err := s.createResourceCreateActivity(ctx, resource); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create activity").SetInternal(err)
} }
return c.JSON(http.StatusOK, composeResponse(resource)) return c.JSON(http.StatusOK, convertResourceFromStore(resource))
}) })
g.GET("/resource", func(c echo.Context) error { g.GET("/resource", func(c echo.Context) error {
@ -282,21 +327,25 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
if !ok { if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session") return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
} }
resourceFind := &api.ResourceFind{ find := &store.FindResource{
CreatorID: &userID, CreatorID: &userID,
} }
if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil { if limit, err := strconv.Atoi(c.QueryParam("limit")); err == nil {
resourceFind.Limit = &limit find.Limit = &limit
} }
if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil { if offset, err := strconv.Atoi(c.QueryParam("offset")); err == nil {
resourceFind.Offset = &offset find.Offset = &offset
} }
list, err := s.Store.FindResourceList(ctx, resourceFind) list, err := s.Store.ListResources(ctx, find)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
} }
return c.JSON(http.StatusOK, composeResponse(list)) resourceMessageList := []*Resource{}
for _, resource := range list {
resourceMessageList = append(resourceMessageList, convertResourceFromStore(resource))
}
return c.JSON(http.StatusOK, resourceMessageList)
}) })
g.PATCH("/resource/:resourceId", func(c echo.Context) error { g.PATCH("/resource/:resourceId", func(c echo.Context) error {
@ -311,10 +360,9 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err) return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
} }
resourceFind := &api.ResourceFind{ resource, err := s.Store.GetResource(ctx, &store.FindResource{
ID: &resourceID, ID: &resourceID,
} })
resource, err := s.Store.FindResource(ctx, resourceFind)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
} }
@ -322,25 +370,29 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized") return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
} }
currentTs := time.Now().Unix() request := &UpdateResourceRequest{}
resourcePatch := &api.ResourcePatch{ if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
UpdatedTs: &currentTs,
}
if err := json.NewDecoder(c.Request().Body).Decode(resourcePatch); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch resource request").SetInternal(err) return echo.NewHTTPError(http.StatusBadRequest, "Malformatted patch resource request").SetInternal(err)
} }
if resourcePatch.ResetPublicID != nil && *resourcePatch.ResetPublicID { currentTs := time.Now().Unix()
update := &store.UpdateResource{
ID: resourceID,
UpdatedTs: &currentTs,
}
if request.Filename != nil && *request.Filename != "" {
update.Filename = request.Filename
}
if request.ResetPublicID != nil && *request.ResetPublicID {
publicID := common.GenUUID() publicID := common.GenUUID()
resourcePatch.PublicID = &publicID update.PublicID = &publicID
} }
resourcePatch.ID = resourceID resource, err = s.Store.UpdateResource(ctx, update)
resource, err = s.Store.PatchResource(ctx, resourcePatch)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch resource").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to patch resource").SetInternal(err)
} }
return c.JSON(http.StatusOK, composeResponse(resource)) return c.JSON(http.StatusOK, convertResourceFromStore(resource))
}) })
g.DELETE("/resource/:resourceId", func(c echo.Context) error { g.DELETE("/resource/:resourceId", func(c echo.Context) error {
@ -355,15 +407,15 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err) return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
} }
resource, err := s.Store.FindResource(ctx, &api.ResourceFind{ resource, err := s.Store.GetResource(ctx, &store.FindResource{
ID: &resourceID, ID: &resourceID,
CreatorID: &userID, CreatorID: &userID,
}) })
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
} }
if resource.CreatorID != userID { if resource == nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized") return echo.NewHTTPError(http.StatusNotFound, "Resource not found")
} }
if resource.InternalPath != "" { if resource.InternalPath != "" {
@ -378,20 +430,16 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
log.Warn(fmt.Sprintf("failed to delete local thumbnail with path %s", thumbnailPath), zap.Error(err)) log.Warn(fmt.Sprintf("failed to delete local thumbnail with path %s", thumbnailPath), zap.Error(err))
} }
resourceDelete := &api.ResourceDelete{ if err := s.Store.DeleteResourceV1(ctx, &store.DeleteResource{
ID: resourceID, ID: resourceID,
} }); err != nil {
if err := s.Store.DeleteResource(ctx, resourceDelete); err != nil {
if common.ErrorCode(err) == common.NotFound {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource ID not found: %d", resourceID))
}
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete resource").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete resource").SetInternal(err)
} }
return c.JSON(http.StatusOK, true) return c.JSON(http.StatusOK, true)
}) })
} }
func (s *Server) registerResourcePublicRoutes(g *echo.Group) { func (s *APIV1Service) registerResourcePublicRoutes(g *echo.Group) {
f := func(c echo.Context) error { f := func(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()
resourceID, err := strconv.Atoi(c.Param("resourceId")) resourceID, err := strconv.Atoi(c.Param("resourceId"))
@ -399,7 +447,7 @@ func (s *Server) registerResourcePublicRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err) return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
} }
resourceVisibility, err := CheckResourceVisibility(ctx, s.Store, resourceID) resourceVisibility, err := checkResourceVisibility(ctx, s.Store, resourceID)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Failed to get resource visibility").SetInternal(err) return echo.NewHTTPError(http.StatusBadRequest, "Failed to get resource visibility").SetInternal(err)
} }
@ -410,11 +458,10 @@ func (s *Server) registerResourcePublicRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusUnauthorized, "Resource visibility not match").SetInternal(err) return echo.NewHTTPError(http.StatusUnauthorized, "Resource visibility not match").SetInternal(err)
} }
resourceFind := &api.ResourceFind{ resource, err := s.Store.GetResource(ctx, &store.FindResource{
ID: &resourceID, ID: &resourceID,
GetBlob: true, GetBlob: true,
} })
resource, err := s.Store.FindResource(ctx, resourceFind)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find resource by ID: %v", resourceID)).SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find resource by ID: %v", resourceID)).SetInternal(err)
} }
@ -465,8 +512,8 @@ func (s *Server) registerResourcePublicRoutes(g *echo.Group) {
g.GET("/r/:resourceId/*", f) g.GET("/r/:resourceId/*", f)
} }
func (s *Server) createResourceCreateActivity(ctx context.Context, resource *api.Resource) error { func (s *APIV1Service) createResourceCreateActivity(ctx context.Context, resource *store.Resource) error {
payload := apiv1.ActivityResourceCreatePayload{ payload := ActivityResourceCreatePayload{
Filename: resource.Filename, Filename: resource.Filename,
Type: resource.Type, Type: resource.Type,
Size: resource.Size, Size: resource.Size,
@ -477,8 +524,8 @@ func (s *Server) createResourceCreateActivity(ctx context.Context, resource *api
} }
activity, err := s.Store.CreateActivity(ctx, &store.ActivityMessage{ activity, err := s.Store.CreateActivity(ctx, &store.ActivityMessage{
CreatorID: resource.CreatorID, CreatorID: resource.CreatorID,
Type: apiv1.ActivityResourceCreate.String(), Type: ActivityResourceCreate.String(),
Level: apiv1.ActivityInfo.String(), Level: ActivityInfo.String(),
Payload: string(payloadBytes), Payload: string(payloadBytes),
}) })
if err != nil || activity == nil { if err != nil || activity == nil {
@ -560,12 +607,10 @@ func getOrGenerateThumbnailImage(srcBlob []byte, dstPath string) ([]byte, error)
return dstBlob, nil return dstBlob, nil
} }
func CheckResourceVisibility(ctx context.Context, s *store.Store, resourceID int) (store.Visibility, error) { func checkResourceVisibility(ctx context.Context, s *store.Store, resourceID int) (store.Visibility, error) {
memoResourceFind := &api.MemoResourceFind{ memoResources, err := s.ListMemoResources(ctx, &store.FindMemoResource{
ResourceID: &resourceID, ResourceID: &resourceID,
} })
memoResources, err := s.FindMemoResourceList(ctx, memoResourceFind)
if err != nil { if err != nil {
return store.Private, err return store.Private, err
} }
@ -604,3 +649,20 @@ func CheckResourceVisibility(ctx context.Context, s *store.Store, resourceID int
// If all memo is PRIVATE, the resource do // If all memo is PRIVATE, the resource do
return store.Private, nil return store.Private, nil
} }
func convertResourceFromStore(resource *store.Resource) *Resource {
return &Resource{
ID: resource.ID,
CreatorID: resource.CreatorID,
CreatedTs: resource.CreatedTs,
UpdatedTs: resource.UpdatedTs,
Filename: resource.Filename,
Blob: resource.Blob,
InternalPath: resource.InternalPath,
ExternalLink: resource.ExternalLink,
Type: resource.Type,
Size: resource.Size,
PublicID: resource.PublicID,
LinkedMemoAmount: resource.LinkedMemoAmount,
}
}

View File

@ -34,4 +34,11 @@ func (s *APIV1Service) Register(rootGroup *echo.Group) {
s.registerTagRoutes(apiV1Group) s.registerTagRoutes(apiV1Group)
s.registerShortcutRoutes(apiV1Group) s.registerShortcutRoutes(apiV1Group)
s.registerStorageRoutes(apiV1Group) s.registerStorageRoutes(apiV1Group)
s.registerResourceRoutes(apiV1Group)
publicGroup := rootGroup.Group("/o")
publicGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return JWTMiddleware(s, next, s.Secret)
})
s.registerResourcePublicRoutes(publicGroup)
} }

View File

@ -685,13 +685,15 @@ func (s *Server) composeMemoMessageToMemoResponse(ctx context.Context, memoMessa
resourceList := []*api.Resource{} resourceList := []*api.Resource{}
for _, resourceID := range memoMessage.ResourceIDList { for _, resourceID := range memoMessage.ResourceIDList {
resource, err := s.Store.FindResource(ctx, &api.ResourceFind{ resource, err := s.Store.GetResource(ctx, &store.FindResource{
ID: &resourceID, ID: &resourceID,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
resourceList = append(resourceList, resource) if resource != nil {
resourceList = append(resourceList, convertResourceFromStore(resource))
}
} }
memoResponse.ResourceList = resourceList memoResponse.ResourceList = resourceList
@ -714,3 +716,20 @@ func (s *Server) getMemoDisplayWithUpdatedTsSettingValue(ctx context.Context) (b
} }
return memoDisplayWithUpdatedTs, nil return memoDisplayWithUpdatedTs, nil
} }
func convertResourceFromStore(resource *store.Resource) *api.Resource {
return &api.Resource{
ID: resource.ID,
CreatorID: resource.CreatorID,
CreatedTs: resource.CreatedTs,
UpdatedTs: resource.UpdatedTs,
Filename: resource.Filename,
Blob: resource.Blob,
InternalPath: resource.InternalPath,
ExternalLink: resource.ExternalLink,
Type: resource.Type,
Size: resource.Size,
PublicID: resource.PublicID,
LinkedMemoAmount: resource.LinkedMemoAmount,
}
}

View File

@ -29,10 +29,9 @@ func (s *Server) registerMemoResourceRoutes(g *echo.Group) {
if err := json.NewDecoder(c.Request().Body).Decode(memoResourceUpsert); err != nil { if err := json.NewDecoder(c.Request().Body).Decode(memoResourceUpsert); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo resource request").SetInternal(err) return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo resource request").SetInternal(err)
} }
resourceFind := &api.ResourceFind{ resource, err := s.Store.GetResource(ctx, &store.FindResource{
ID: &memoResourceUpsert.ResourceID, ID: &memoResourceUpsert.ResourceID,
} })
resource, err := s.Store.FindResource(ctx, resourceFind)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
} }
@ -48,7 +47,7 @@ func (s *Server) registerMemoResourceRoutes(g *echo.Group) {
if _, err := s.Store.UpsertMemoResource(ctx, memoResourceUpsert); err != nil { if _, err := s.Store.UpsertMemoResource(ctx, memoResourceUpsert); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
} }
return c.JSON(http.StatusOK, composeResponse(resource)) return c.JSON(http.StatusOK, true)
}) })
g.GET("/memo/:memoId/resource", func(c echo.Context) error { g.GET("/memo/:memoId/resource", func(c echo.Context) error {
@ -58,13 +57,16 @@ func (s *Server) registerMemoResourceRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err) return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
} }
resourceFind := &api.ResourceFind{ list, err := s.Store.ListResources(ctx, &store.FindResource{
MemoID: &memoID, MemoID: &memoID,
} })
resourceList, err := s.Store.FindResourceList(ctx, resourceFind)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
} }
resourceList := []*api.Resource{}
for _, resource := range list {
resourceList = append(resourceList, convertResourceFromStore(resource))
}
return c.JSON(http.StatusOK, composeResponse(resourceList)) return c.JSON(http.StatusOK, composeResponse(resourceList))
}) })

View File

@ -11,7 +11,6 @@ import (
"github.com/gorilla/feeds" "github.com/gorilla/feeds"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/usememos/memos/api"
apiv1 "github.com/usememos/memos/api/v1" apiv1 "github.com/usememos/memos/api/v1"
"github.com/usememos/memos/common" "github.com/usememos/memos/common"
"github.com/usememos/memos/store" "github.com/usememos/memos/store"
@ -102,7 +101,7 @@ func (s *Server) generateRSSFromMemoList(ctx context.Context, memoList []*store.
} }
if len(memo.ResourceIDList) > 0 { if len(memo.ResourceIDList) > 0 {
resourceID := memo.ResourceIDList[0] resourceID := memo.ResourceIDList[0]
resource, err := s.Store.FindResource(ctx, &api.ResourceFind{ resource, err := s.Store.GetResource(ctx, &store.FindResource{
ID: &resourceID, ID: &resourceID,
}) })
if err != nil { if err != nil {

View File

@ -92,7 +92,6 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store
return JWTMiddleware(s, next, s.Secret) return JWTMiddleware(s, next, s.Secret)
}) })
registerGetterPublicRoutes(publicGroup) registerGetterPublicRoutes(publicGroup)
s.registerResourcePublicRoutes(publicGroup)
apiGroup := e.Group("/api") apiGroup := e.Group("/api")
apiGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc { apiGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
@ -100,7 +99,6 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store
}) })
s.registerMemoRoutes(apiGroup) s.registerMemoRoutes(apiGroup)
s.registerMemoResourceRoutes(apiGroup) s.registerMemoResourceRoutes(apiGroup)
s.registerResourceRoutes(apiGroup)
s.registerMemoRelationRoutes(apiGroup) s.registerMemoRelationRoutes(apiGroup)
apiV1Service := apiv1.NewAPIV1Service(s.Secret, profile, store) apiV1Service := apiv1.NewAPIV1Service(s.Secret, profile, store)

View File

@ -90,15 +90,14 @@ func (t *telegramHandler) MessageHandle(ctx context.Context, bot *telegram.Bot,
case ".png": case ".png":
mime = "image/png" mime = "image/png"
} }
resourceCreate := api.ResourceCreate{ resource, err := t.store.CreateResourceV1(ctx, &store.Resource{
CreatorID: creatorID, CreatorID: creatorID,
Filename: filename, Filename: filename,
Type: mime, Type: mime,
Size: int64(len(blob)), Size: int64(len(blob)),
Blob: blob, Blob: blob,
PublicID: common.GenUUID(), PublicID: common.GenUUID(),
} })
resource, err := t.store.CreateResource(ctx, &resourceCreate)
if err != nil { if err != nil {
_, err := bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("failed to CreateResource: %s", err), nil) _, err := bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("failed to CreateResource: %s", err), nil)
return err return err

View File

@ -10,6 +10,85 @@ import (
"github.com/usememos/memos/common" "github.com/usememos/memos/common"
) )
type MemoResource struct {
MemoID int
ResourceID int
CreatedTs int64
UpdatedTs int64
}
type FindMemoResource struct {
MemoID *int
ResourceID *int
}
func (s *Store) ListMemoResources(ctx context.Context, find *FindMemoResource) ([]*MemoResource, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer tx.Rollback()
list, err := listMemoResources(ctx, tx, find)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
return list, nil
}
func listMemoResources(ctx context.Context, tx *sql.Tx, find *FindMemoResource) ([]*MemoResource, error) {
where, args := []string{"1 = 1"}, []any{}
if v := find.MemoID; v != nil {
where, args = append(where, "memo_id = ?"), append(args, *v)
}
if v := find.ResourceID; v != nil {
where, args = append(where, "resource_id = ?"), append(args, *v)
}
query := `
SELECT
memo_id,
resource_id,
created_ts,
updated_ts
FROM memo_resource
WHERE ` + strings.Join(where, " AND ") + `
ORDER BY updated_ts DESC
`
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return nil, FormatError(err)
}
defer rows.Close()
list := make([]*MemoResource, 0)
for rows.Next() {
var memoResource MemoResource
if err := rows.Scan(
&memoResource.MemoID,
&memoResource.ResourceID,
&memoResource.CreatedTs,
&memoResource.UpdatedTs,
); err != nil {
return nil, FormatError(err)
}
list = append(list, &memoResource)
}
if err := rows.Err(); err != nil {
return nil, err
}
return list, nil
}
// memoResourceRaw is the store model for an MemoResource. // memoResourceRaw is the store model for an MemoResource.
// Fields have exactly the same meanings as MemoResource. // Fields have exactly the same meanings as MemoResource.
type memoResourceRaw struct { type memoResourceRaw struct {

View File

@ -5,14 +5,9 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"strings" "strings"
"github.com/usememos/memos/api"
"github.com/usememos/memos/common"
) )
// resourceRaw is the store model for an Resource. type Resource struct {
// Fields have exactly the same meanings as Resource.
type resourceRaw struct {
ID int ID int
// Standard fields // Standard fields
@ -31,217 +26,177 @@ type resourceRaw struct {
LinkedMemoAmount int LinkedMemoAmount int
} }
func (raw *resourceRaw) toResource() *api.Resource { type FindResource struct {
return &api.Resource{ GetBlob bool
ID: raw.ID, ID *int
CreatorID *int
// Standard fields Filename *string
CreatorID: raw.CreatorID, MemoID *int
CreatedTs: raw.CreatedTs, PublicID *string
UpdatedTs: raw.UpdatedTs, Limit *int
Offset *int
// Domain specific fields
Filename: raw.Filename,
Blob: raw.Blob,
InternalPath: raw.InternalPath,
ExternalLink: raw.ExternalLink,
Type: raw.Type,
Size: raw.Size,
PublicID: raw.PublicID,
LinkedMemoAmount: raw.LinkedMemoAmount,
}
} }
func (s *Store) CreateResource(ctx context.Context, create *api.ResourceCreate) (*api.Resource, error) { type UpdateResource struct {
ID int
UpdatedTs *int64
Filename *string
PublicID *string
}
type DeleteResource struct {
ID int
}
func (s *Store) CreateResourceV1(ctx context.Context, create *Resource) (*Resource, error) {
tx, err := s.db.BeginTx(ctx, nil) tx, err := s.db.BeginTx(ctx, nil)
if err != nil { if err != nil {
return nil, FormatError(err) return nil, FormatError(err)
} }
defer tx.Rollback() defer tx.Rollback()
resourceRaw, err := createResourceImpl(ctx, tx, create) if err := tx.QueryRowContext(ctx, `
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
resource := resourceRaw.toResource()
return resource, nil
}
func (s *Store) PatchResource(ctx context.Context, patch *api.ResourcePatch) (*api.Resource, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
resourceRaw, err := patchResourceImpl(ctx, tx, patch)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, FormatError(err)
}
s.resourceCache.Store(resourceRaw.ID, resourceRaw)
resource := resourceRaw.toResource()
return resource, nil
}
func (s *Store) FindResourceList(ctx context.Context, find *api.ResourceFind) ([]*api.Resource, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
resourceRawList, err := findResourceListImpl(ctx, tx, find)
if err != nil {
return nil, err
}
resourceList := []*api.Resource{}
for _, raw := range resourceRawList {
if !find.GetBlob {
s.resourceCache.Store(raw.ID, raw)
}
resourceList = append(resourceList, raw.toResource())
}
return resourceList, nil
}
func (s *Store) FindResource(ctx context.Context, find *api.ResourceFind) (*api.Resource, error) {
if !find.GetBlob && find.ID != nil {
if raw, ok := s.resourceCache.Load(find.ID); ok {
return raw.(*resourceRaw).toResource(), nil
}
}
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
list, err := findResourceListImpl(ctx, tx, find)
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, &common.Error{Code: common.NotFound, Err: fmt.Errorf("not found")}
}
resourceRaw := list[0]
if !find.GetBlob {
s.resourceCache.Store(resourceRaw.ID, resourceRaw)
}
resource := resourceRaw.toResource()
return resource, nil
}
func (s *Store) DeleteResource(ctx context.Context, delete *api.ResourceDelete) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return FormatError(err)
}
defer tx.Rollback()
if err := deleteResource(ctx, tx, delete); err != nil {
return err
}
if err := s.vacuumImpl(ctx, tx); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return FormatError(err)
}
s.resourceCache.Delete(delete.ID)
return nil
}
func createResourceImpl(ctx context.Context, tx *sql.Tx, create *api.ResourceCreate) (*resourceRaw, error) {
fields := []string{"filename", "blob", "external_link", "type", "size", "creator_id", "internal_path", "public_id"}
values := []any{create.Filename, create.Blob, create.ExternalLink, create.Type, create.Size, create.CreatorID, create.InternalPath, create.PublicID}
placeholders := []string{"?", "?", "?", "?", "?", "?", "?", "?"}
query := `
INSERT INTO resource ( INSERT INTO resource (
` + strings.Join(fields, ",") + ` filename,
blob,
external_link,
type,
size,
creator_id,
internal_path,
public_id
) )
VALUES (` + strings.Join(placeholders, ",") + `) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
RETURNING id, ` + strings.Join(fields, ",") + `, created_ts, updated_ts RETURNING id, created_ts, updated_ts
` `,
var resourceRaw resourceRaw create.Filename, create.Blob, create.ExternalLink, create.Type, create.Size, create.CreatorID, create.InternalPath, create.PublicID,
dests := []any{ ).Scan(&create.ID, &create.CreatedTs, &create.UpdatedTs); err != nil {
&resourceRaw.ID, return nil, err
&resourceRaw.Filename,
&resourceRaw.Blob,
&resourceRaw.ExternalLink,
&resourceRaw.Type,
&resourceRaw.Size,
&resourceRaw.CreatorID,
&resourceRaw.InternalPath,
&resourceRaw.PublicID,
}
dests = append(dests, []any{&resourceRaw.CreatedTs, &resourceRaw.UpdatedTs}...)
if err := tx.QueryRowContext(ctx, query, values...).Scan(dests...); err != nil {
return nil, FormatError(err)
} }
return &resourceRaw, nil if err := tx.Commit(); err != nil {
return nil, err
}
resource := create
return resource, nil
} }
func patchResourceImpl(ctx context.Context, tx *sql.Tx, patch *api.ResourcePatch) (*resourceRaw, error) { func (s *Store) ListResources(ctx context.Context, find *FindResource) ([]*Resource, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
resources, err := listResources(ctx, tx, find)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
return resources, nil
}
func (s *Store) GetResource(ctx context.Context, find *FindResource) (*Resource, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
resources, err := listResources(ctx, tx, find)
if err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
if len(resources) == 0 {
return nil, nil
}
return resources[0], nil
}
func (s *Store) UpdateResource(ctx context.Context, update *UpdateResource) (*Resource, error) {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return nil, FormatError(err)
}
defer tx.Rollback()
set, args := []string{}, []any{} set, args := []string{}, []any{}
if v := patch.UpdatedTs; v != nil { if v := update.UpdatedTs; v != nil {
set, args = append(set, "updated_ts = ?"), append(args, *v) set, args = append(set, "updated_ts = ?"), append(args, *v)
} }
if v := patch.Filename; v != nil { if v := update.Filename; v != nil {
set, args = append(set, "filename = ?"), append(args, *v) set, args = append(set, "filename = ?"), append(args, *v)
} }
if v := patch.PublicID; v != nil { if v := update.PublicID; v != nil {
set, args = append(set, "public_id = ?"), append(args, *v) set, args = append(set, "public_id = ?"), append(args, *v)
} }
args = append(args, patch.ID) args = append(args, update.ID)
fields := []string{"id", "filename", "external_link", "type", "size", "creator_id", "created_ts", "updated_ts", "internal_path", "public_id"} fields := []string{"id", "filename", "external_link", "type", "size", "creator_id", "created_ts", "updated_ts", "internal_path", "public_id"}
query := ` query := `
UPDATE resource UPDATE resource
SET ` + strings.Join(set, ", ") + ` SET ` + strings.Join(set, ", ") + `
WHERE id = ? WHERE id = ?
RETURNING ` + strings.Join(fields, ", ") RETURNING ` + strings.Join(fields, ", ")
var resourceRaw resourceRaw resource := Resource{}
dests := []any{ dests := []any{
&resourceRaw.ID, &resource.ID,
&resourceRaw.Filename, &resource.Filename,
&resourceRaw.ExternalLink, &resource.ExternalLink,
&resourceRaw.Type, &resource.Type,
&resourceRaw.Size, &resource.Size,
&resourceRaw.CreatorID, &resource.CreatorID,
&resourceRaw.CreatedTs, &resource.CreatedTs,
&resourceRaw.UpdatedTs, &resource.UpdatedTs,
&resourceRaw.InternalPath, &resource.InternalPath,
&resourceRaw.PublicID, &resource.PublicID,
} }
if err := tx.QueryRowContext(ctx, query, args...).Scan(dests...); err != nil { if err := tx.QueryRowContext(ctx, query, args...).Scan(dests...); err != nil {
return nil, FormatError(err) return nil, FormatError(err)
} }
return &resourceRaw, nil if err := tx.Commit(); err != nil {
return nil, err
}
return &resource, nil
} }
func findResourceListImpl(ctx context.Context, tx *sql.Tx, find *api.ResourceFind) ([]*resourceRaw, error) { func (s *Store) DeleteResourceV1(ctx context.Context, delete *DeleteResource) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return FormatError(err)
}
defer tx.Rollback()
if _, err := tx.ExecContext(ctx, `
DELETE FROM resource
WHERE id = ?
`, delete.ID); err != nil {
return err
}
if err := tx.Commit(); err != nil {
// Prevent linter warning.
return err
}
return nil
}
func listResources(ctx context.Context, tx *sql.Tx, find *FindResource) ([]*Resource, error) {
where, args := []string{"1 = 1"}, []any{} where, args := []string{"1 = 1"}, []any{}
if v := find.ID; v != nil { if v := find.ID; v != nil {
@ -288,53 +243,36 @@ func findResourceListImpl(ctx context.Context, tx *sql.Tx, find *api.ResourceFin
} }
defer rows.Close() defer rows.Close()
resourceRawList := make([]*resourceRaw, 0) list := make([]*Resource, 0)
for rows.Next() { for rows.Next() {
var resourceRaw resourceRaw resource := Resource{}
dests := []any{ dests := []any{
&resourceRaw.LinkedMemoAmount, &resource.LinkedMemoAmount,
&resourceRaw.ID, &resource.ID,
&resourceRaw.Filename, &resource.Filename,
&resourceRaw.ExternalLink, &resource.ExternalLink,
&resourceRaw.Type, &resource.Type,
&resourceRaw.Size, &resource.Size,
&resourceRaw.CreatorID, &resource.CreatorID,
&resourceRaw.CreatedTs, &resource.CreatedTs,
&resourceRaw.UpdatedTs, &resource.UpdatedTs,
&resourceRaw.InternalPath, &resource.InternalPath,
&resourceRaw.PublicID, &resource.PublicID,
} }
if find.GetBlob { if find.GetBlob {
dests = append(dests, &resourceRaw.Blob) dests = append(dests, &resource.Blob)
} }
if err := rows.Scan(dests...); err != nil { if err := rows.Scan(dests...); err != nil {
return nil, FormatError(err) return nil, FormatError(err)
} }
resourceRawList = append(resourceRawList, &resourceRaw) list = append(list, &resource)
} }
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
return nil, FormatError(err) return nil, FormatError(err)
} }
return resourceRawList, nil return list, nil
}
func deleteResource(ctx context.Context, tx *sql.Tx, delete *api.ResourceDelete) error {
where, args := []string{"id = ?"}, []any{delete.ID}
stmt := `DELETE FROM resource WHERE ` + strings.Join(where, " AND ")
result, err := tx.ExecContext(ctx, stmt, args...)
if err != nil {
return FormatError(err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("resource not found")}
}
return nil
} }
func vacuumResource(ctx context.Context, tx *sql.Tx) error { func vacuumResource(ctx context.Context, tx *sql.Tx) error {

View File

@ -17,7 +17,6 @@ type Store struct {
userSettingCache sync.Map // map[string]*UserSetting userSettingCache sync.Map // map[string]*UserSetting
shortcutCache sync.Map // map[int]*shortcutRaw shortcutCache sync.Map // map[int]*shortcutRaw
idpCache sync.Map // map[int]*IdentityProvider idpCache sync.Map // map[int]*IdentityProvider
resourceCache sync.Map // map[int]*resourceRaw
} }
// New creates a new instance of Store. // New creates a new instance of Store.

View File

@ -5,13 +5,13 @@ import (
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/usememos/memos/api" "github.com/usememos/memos/store"
) )
func TestResourceStore(t *testing.T) { func TestResourceStore(t *testing.T) {
ctx := context.Background() ctx := context.Background()
store := NewTestingStore(ctx, t) ts := NewTestingStore(ctx, t)
_, err := store.CreateResource(ctx, &api.ResourceCreate{ _, err := ts.CreateResourceV1(ctx, &store.Resource{
CreatorID: 101, CreatorID: 101,
Filename: "test.epub", Filename: "test.epub",
Blob: []byte("test"), Blob: []byte("test"),
@ -25,34 +25,36 @@ func TestResourceStore(t *testing.T) {
correctFilename := "test.epub" correctFilename := "test.epub"
incorrectFilename := "test.png" incorrectFilename := "test.png"
res, err := store.FindResource(ctx, &api.ResourceFind{ res, err := ts.GetResource(ctx, &store.FindResource{
Filename: &correctFilename, Filename: &correctFilename,
}) })
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, correctFilename, res.Filename) require.Equal(t, correctFilename, res.Filename)
require.Equal(t, 1, res.ID) require.Equal(t, 1, res.ID)
_, err = store.FindResource(ctx, &api.ResourceFind{ notFoundResource, err := ts.GetResource(ctx, &store.FindResource{
Filename: &incorrectFilename, Filename: &incorrectFilename,
}) })
require.Error(t, err) require.NoError(t, err)
require.Nil(t, notFoundResource)
correctCreatorID := 101 correctCreatorID := 101
incorrectCreatorID := 102 incorrectCreatorID := 102
_, err = store.FindResource(ctx, &api.ResourceFind{ _, err = ts.GetResource(ctx, &store.FindResource{
CreatorID: &correctCreatorID, CreatorID: &correctCreatorID,
}) })
require.NoError(t, err) require.NoError(t, err)
_, err = store.FindResource(ctx, &api.ResourceFind{ notFoundResource, err = ts.GetResource(ctx, &store.FindResource{
CreatorID: &incorrectCreatorID, CreatorID: &incorrectCreatorID,
}) })
require.Error(t, err) require.NoError(t, err)
require.Nil(t, notFoundResource)
err = store.DeleteResource(ctx, &api.ResourceDelete{ err = ts.DeleteResourceV1(ctx, &store.DeleteResource{
ID: 1, ID: 1,
}) })
require.NoError(t, err) require.NoError(t, err)
err = store.DeleteResource(ctx, &api.ResourceDelete{ err = ts.DeleteResourceV1(ctx, &store.DeleteResource{
ID: 2, ID: 2,
}) })
require.Error(t, err) require.NoError(t, err)
} }

View File

@ -161,7 +161,7 @@ export function deleteShortcutById(shortcutId: ShortcutId) {
} }
export function getResourceList() { export function getResourceList() {
return axios.get<ResponseObject<Resource[]>>("/api/resource"); return axios.get<Resource[]>("/api/v1/resource");
} }
export function getResourceListWithLimit(resourceFind?: ResourceFind) { export function getResourceListWithLimit(resourceFind?: ResourceFind) {
@ -172,23 +172,23 @@ export function getResourceListWithLimit(resourceFind?: ResourceFind) {
if (resourceFind?.limit) { if (resourceFind?.limit) {
queryList.push(`limit=${resourceFind.limit}`); queryList.push(`limit=${resourceFind.limit}`);
} }
return axios.get<ResponseObject<Resource[]>>(`/api/resource?${queryList.join("&")}`); return axios.get<Resource[]>(`/api/v1/resource?${queryList.join("&")}`);
} }
export function createResource(resourceCreate: ResourceCreate) { export function createResource(resourceCreate: ResourceCreate) {
return axios.post<ResponseObject<Resource>>("/api/resource", resourceCreate); return axios.post<Resource>("/api/v1/resource", resourceCreate);
} }
export function createResourceWithBlob(formData: FormData) { export function createResourceWithBlob(formData: FormData) {
return axios.post<ResponseObject<Resource>>("/api/resource/blob", formData); return axios.post<Resource>("/api/v1/resource/blob", formData);
}
export function deleteResourceById(id: ResourceId) {
return axios.delete(`/api/resource/${id}`);
} }
export function patchResource(resourcePatch: ResourcePatch) { export function patchResource(resourcePatch: ResourcePatch) {
return axios.patch<ResponseObject<Resource>>(`/api/resource/${resourcePatch.id}`, resourcePatch); return axios.patch<Resource>(`/api/v1/resource/${resourcePatch.id}`, resourcePatch);
}
export function deleteResourceById(id: ResourceId) {
return axios.delete(`/api/v1/resource/${id}`);
} }
export function getMemoResourceList(memoId: MemoId) { export function getMemoResourceList(memoId: MemoId) {
@ -196,7 +196,7 @@ export function getMemoResourceList(memoId: MemoId) {
} }
export function upsertMemoResource(memoId: MemoId, resourceId: ResourceId) { export function upsertMemoResource(memoId: MemoId, resourceId: ResourceId) {
return axios.post<ResponseObject<Resource>>(`/api/memo/${memoId}/resource`, { return axios.post(`/api/memo/${memoId}/resource`, {
resourceId, resourceId,
}); });
} }

View File

@ -25,7 +25,7 @@ export const useResourceStore = () => {
return store.getState().resource; return store.getState().resource;
}, },
async fetchResourceList(): Promise<Resource[]> { async fetchResourceList(): Promise<Resource[]> {
const { data } = (await api.getResourceList()).data; const { data } = await api.getResourceList();
const resourceList = data.map((m) => convertResponseModelResource(m)); const resourceList = data.map((m) => convertResponseModelResource(m));
store.dispatch(setResources(resourceList)); store.dispatch(setResources(resourceList));
return resourceList; return resourceList;
@ -35,13 +35,13 @@ export const useResourceStore = () => {
limit, limit,
offset, offset,
}; };
const { data } = (await api.getResourceListWithLimit(resourceFind)).data; const { data } = await api.getResourceListWithLimit(resourceFind);
const resourceList = data.map((m) => convertResponseModelResource(m)); const resourceList = data.map((m) => convertResponseModelResource(m));
store.dispatch(upsertResources(resourceList)); store.dispatch(upsertResources(resourceList));
return resourceList; return resourceList;
}, },
async createResource(resourceCreate: ResourceCreate): Promise<Resource> { async createResource(resourceCreate: ResourceCreate): Promise<Resource> {
const { data } = (await api.createResource(resourceCreate)).data; const { data } = await api.createResource(resourceCreate);
const resource = convertResponseModelResource(data); const resource = convertResponseModelResource(data);
const resourceList = state.resources; const resourceList = state.resources;
store.dispatch(setResources([resource, ...resourceList])); store.dispatch(setResources([resource, ...resourceList]));
@ -55,7 +55,7 @@ export const useResourceStore = () => {
const formData = new FormData(); const formData = new FormData();
formData.append("file", file, filename); formData.append("file", file, filename);
const { data } = (await api.createResourceWithBlob(formData)).data; const { data } = await api.createResourceWithBlob(formData);
const resource = convertResponseModelResource(data); const resource = convertResponseModelResource(data);
const resourceList = state.resources; const resourceList = state.resources;
store.dispatch(setResources([resource, ...resourceList])); store.dispatch(setResources([resource, ...resourceList]));
@ -71,7 +71,7 @@ export const useResourceStore = () => {
const formData = new FormData(); const formData = new FormData();
formData.append("file", file, filename); formData.append("file", file, filename);
const { data } = (await api.createResourceWithBlob(formData)).data; const { data } = await api.createResourceWithBlob(formData);
const resource = convertResponseModelResource(data); const resource = convertResponseModelResource(data);
newResourceList = [resource, ...newResourceList]; newResourceList = [resource, ...newResourceList];
} }
@ -84,7 +84,7 @@ export const useResourceStore = () => {
store.dispatch(deleteResource(id)); store.dispatch(deleteResource(id));
}, },
async patchResource(resourcePatch: ResourcePatch): Promise<Resource> { async patchResource(resourcePatch: ResourcePatch): Promise<Resource> {
const { data } = (await api.patchResource(resourcePatch)).data; const { data } = await api.patchResource(resourcePatch);
const resource = convertResponseModelResource(data); const resource = convertResponseModelResource(data);
store.dispatch(patchResource(resource)); store.dispatch(patchResource(resource));
return resource; return resource;