chore: retire memo resource relation table

This commit is contained in:
Steven 2023-09-27 00:40:16 +08:00
parent 4f10198ec0
commit 6007f48b7d
21 changed files with 126 additions and 567 deletions

View file

@ -319,9 +319,9 @@ func (s *APIV1Service) CreateMemo(c echo.Context) error {
}
for _, resourceID := range createMemoRequest.ResourceIDList {
if _, err := s.Store.UpsertMemoResource(ctx, &store.UpsertMemoResource{
MemoID: memo.ID,
ResourceID: resourceID,
if _, err := s.Store.UpdateResource(ctx, &store.UpdateResource{
ID: resourceID,
MemoID: &memo.ID,
}); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
}
@ -694,19 +694,18 @@ func (s *APIV1Service) UpdateMemo(c echo.Context) error {
if patchMemoRequest.ResourceIDList != nil {
addedResourceIDList, removedResourceIDList := getIDListDiff(memo.ResourceIDList, patchMemoRequest.ResourceIDList)
for _, resourceID := range addedResourceIDList {
if _, err := s.Store.UpsertMemoResource(ctx, &store.UpsertMemoResource{
MemoID: memo.ID,
ResourceID: resourceID,
if _, err := s.Store.UpdateResource(ctx, &store.UpdateResource{
ID: resourceID,
MemoID: &memo.ID,
}); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
}
}
for _, resourceID := range removedResourceIDList {
if err := s.Store.DeleteMemoResource(ctx, &store.DeleteMemoResource{
MemoID: &memo.ID,
ResourceID: &resourceID,
if err := s.Store.DeleteResource(ctx, &store.DeleteResource{
ID: resourceID,
}); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete memo resource").SetInternal(err)
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to delete resource").SetInternal(err)
}
}
}

View file

@ -1,179 +0,0 @@
package v1
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/labstack/echo/v4"
"github.com/usememos/memos/common/util"
"github.com/usememos/memos/store"
)
type MemoResource struct {
MemoID int32 `json:"memoId"`
ResourceID int32 `json:"resourceId"`
CreatedTs int64 `json:"createdTs"`
UpdatedTs int64 `json:"updatedTs"`
}
type UpsertMemoResourceRequest struct {
ResourceID int32 `json:"resourceId"`
UpdatedTs *int64 `json:"updatedTs"`
}
type MemoResourceFind struct {
MemoID *int32
ResourceID *int32
}
type MemoResourceDelete struct {
MemoID *int32
ResourceID *int32
}
func (s *APIV1Service) registerMemoResourceRoutes(g *echo.Group) {
g.GET("/memo/:memoId/resource", s.GetMemoResourceList)
g.POST("/memo/:memoId/resource", s.BindMemoResource)
g.DELETE("/memo/:memoId/resource/:resourceId", s.UnbindMemoResource)
}
// GetMemoResourceList godoc
//
// @Summary Get resource list of a memo
// @Tags memo-resource
// @Accept json
// @Produce json
// @Param memoId path int true "ID of memo to fetch resource list from"
// @Success 200 {object} []Resource "Memo resource list"
// @Failure 400 {object} nil "ID is not a number: %s"
// @Failure 500 {object} nil "Failed to fetch resource list"
// @Router /api/v1/memo/{memoId}/resource [GET]
func (s *APIV1Service) GetMemoResourceList(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
list, err := s.Store.ListResources(ctx, &store.FindResource{
MemoID: &memoID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
}
resourceList := []*Resource{}
for _, resource := range list {
resourceList = append(resourceList, convertResourceFromStore(resource))
}
return c.JSON(http.StatusOK, resourceList)
}
// BindMemoResource godoc
//
// @Summary Bind resource to memo
// @Tags memo-resource
// @Accept json
// @Produce json
// @Param memoId path int true "ID of memo to bind resource to"
// @Param body body UpsertMemoResourceRequest true "Memo resource request object"
// @Success 200 {boolean} true "Memo resource binded"
// @Failure 400 {object} nil "ID is not a number: %s | Malformatted post memo resource request | Resource not found"
// @Failure 401 {object} nil "Missing user in session | Unauthorized to bind this resource"
// @Failure 500 {object} nil "Failed to fetch resource | Failed to upsert memo resource"
// @Router /api/v1/memo/{memoId}/resource [POST]
//
// NOTES:
// - Passing 0 to updatedTs will set it to 0 in the database, which is probably unwanted.
func (s *APIV1Service) BindMemoResource(c echo.Context) error {
ctx := c.Request().Context()
memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
request := &UpsertMemoResourceRequest{}
if err := json.NewDecoder(c.Request().Body).Decode(request); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post memo resource request").SetInternal(err)
}
resource, err := s.Store.GetResource(ctx, &store.FindResource{
ID: &request.ResourceID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource").SetInternal(err)
}
if resource == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Resource not found").SetInternal(err)
} else if resource.CreatorID != userID {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized to bind this resource").SetInternal(err)
}
upsert := &store.UpsertMemoResource{
MemoID: memoID,
ResourceID: request.ResourceID,
CreatedTs: time.Now().Unix(),
}
if request.UpdatedTs != nil {
upsert.UpdatedTs = request.UpdatedTs
}
if _, err := s.Store.UpsertMemoResource(ctx, upsert); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upsert memo resource").SetInternal(err)
}
return c.JSON(http.StatusOK, true)
}
// UnbindMemoResource godoc
//
// @Summary Unbind resource from memo
// @Tags memo-resource
// @Accept json
// @Produce json
// @Param memoId path int true "ID of memo to unbind resource from"
// @Param resourceId path int true "ID of resource to unbind from memo"
// @Success 200 {boolean} true "Memo resource unbinded. *200 is returned even if the reference doesn't exists "
// @Failure 400 {object} nil "Memo ID is not a number: %s | Resource ID is not a number: %s | Memo not found"
// @Failure 401 {object} nil "Missing user in session | Unauthorized"
// @Failure 500 {object} nil "Failed to find memo | Failed to fetch resource list"
// @Router /api/v1/memo/{memoId}/resource/{resourceId} [DELETE]
func (s *APIV1Service) UnbindMemoResource(c echo.Context) error {
ctx := c.Request().Context()
userID, ok := c.Get(userIDContextKey).(int32)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
}
memoID, err := util.ConvertStringToInt32(c.Param("memoId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Memo ID is not a number: %s", c.Param("memoId"))).SetInternal(err)
}
resourceID, err := util.ConvertStringToInt32(c.Param("resourceId"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Resource ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
}
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
ID: &memoID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo").SetInternal(err)
}
if memo == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Memo not found")
}
if memo.CreatorID != userID {
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
}
if err := s.Store.DeleteMemoResource(ctx, &store.DeleteMemoResource{
MemoID: &memoID,
ResourceID: &resourceID,
}); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch resource list").SetInternal(err)
}
return c.JSON(http.StatusOK, true)
}

View file

@ -384,17 +384,6 @@ func (s *APIV1Service) streamResource(c echo.Context) error {
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)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Failed to get resource visibility").SetInternal(err)
}
// Protected resource require a logined user
userID, ok := c.Get(userIDContextKey).(int32)
if resourceVisibility == store.Protected && (!ok || userID <= 0) {
return echo.NewHTTPError(http.StatusUnauthorized, "Resource visibility not match").SetInternal(err)
}
resource, err := s.Store.GetResource(ctx, &store.FindResource{
ID: &resourceID,
GetBlob: true,
@ -405,10 +394,20 @@ func (s *APIV1Service) streamResource(c echo.Context) error {
if resource == nil {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource not found: %d", resourceID))
}
// Private resource require logined user is the creator
if resourceVisibility == store.Private && (!ok || userID != resource.CreatorID) {
return echo.NewHTTPError(http.StatusUnauthorized, "Resource visibility not match").SetInternal(err)
// Check the related memo visibility.
if resource.MemoID != nil {
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
ID: resource.MemoID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find memo by ID: %v", resource.MemoID)).SetInternal(err)
}
if memo != nil && memo.Visibility != store.Public {
userID, ok := c.Get(userIDContextKey).(int32)
if !ok || (memo.Visibility == store.Private && userID != resource.CreatorID) {
return echo.NewHTTPError(http.StatusUnauthorized, "Resource visibility not match")
}
}
}
blob := resource.Blob
@ -542,47 +541,6 @@ func getOrGenerateThumbnailImage(srcBlob []byte, dstPath string) ([]byte, error)
return dstBlob, nil
}
func checkResourceVisibility(ctx context.Context, s *store.Store, resourceID int32) (store.Visibility, error) {
memoResources, err := s.ListMemoResources(ctx, &store.FindMemoResource{
ResourceID: &resourceID,
})
if err != nil {
return store.Private, err
}
// If resource is belongs to no memo, it'll always PRIVATE.
if len(memoResources) == 0 {
return store.Private, nil
}
memoIDs := make([]int32, 0, len(memoResources))
for _, memoResource := range memoResources {
memoIDs = append(memoIDs, memoResource.MemoID)
}
visibilityList, err := s.FindMemosVisibilityList(ctx, memoIDs)
if err != nil {
return store.Private, err
}
var isProtected bool
for _, visibility := range visibilityList {
// If any memo is PUBLIC, resource should be PUBLIC too.
if visibility == store.Public {
return store.Public, nil
}
if visibility == store.Protected {
isProtected = true
}
}
if isProtected {
return store.Protected, nil
}
return store.Private, nil
}
func convertResourceFromStore(resource *store.Resource) *Resource {
return &Resource{
ID: resource.ID,

View file

@ -58,7 +58,6 @@ func (s *APIV1Service) Register(rootGroup *echo.Group) {
s.registerResourceRoutes(apiV1Group)
s.registerMemoRoutes(apiV1Group)
s.registerMemoOrganizerRoutes(apiV1Group)
s.registerMemoResourceRoutes(apiV1Group)
s.registerMemoRelationRoutes(apiV1Group)
// Register public routes.

View file

@ -47,12 +47,12 @@ func (s *ResourceService) ListResources(ctx context.Context, _ *apiv2pb.ListReso
func convertResourceFromStore(resource *store.Resource) *apiv2pb.Resource {
return &apiv2pb.Resource{
Id: resource.ID,
CreatedTs: timestamppb.New(time.Unix(resource.CreatedTs, 0)),
Filename: resource.Filename,
ExternalLink: resource.ExternalLink,
Type: resource.Type,
Size: resource.Size,
RelatedMemoId: resource.RelatedMemoID,
Id: resource.ID,
CreatedTs: timestamppb.New(time.Unix(resource.CreatedTs, 0)),
Filename: resource.Filename,
ExternalLink: resource.ExternalLink,
Type: resource.Type,
Size: resource.Size,
MemoId: resource.MemoID,
}
}

View file

@ -20,7 +20,7 @@ message Resource {
string external_link = 4;
string type = 5;
int64 size = 6;
optional int32 related_memo_id = 7;
optional int32 memo_id = 7;
}
message ListResourcesRequest {}

View file

@ -262,7 +262,7 @@
| external_link | [string](#string) | | |
| type | [string](#string) | | |
| size | [int64](#int64) | | |
| related_memo_id | [int32](#int32) | optional | |
| memo_id | [int32](#int32) | optional | |

View file

@ -27,13 +27,13 @@ type Resource struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
CreatedTs *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=created_ts,json=createdTs,proto3" json:"created_ts,omitempty"`
Filename string `protobuf:"bytes,3,opt,name=filename,proto3" json:"filename,omitempty"`
ExternalLink string `protobuf:"bytes,4,opt,name=external_link,json=externalLink,proto3" json:"external_link,omitempty"`
Type string `protobuf:"bytes,5,opt,name=type,proto3" json:"type,omitempty"`
Size int64 `protobuf:"varint,6,opt,name=size,proto3" json:"size,omitempty"`
RelatedMemoId *int32 `protobuf:"varint,7,opt,name=related_memo_id,json=relatedMemoId,proto3,oneof" json:"related_memo_id,omitempty"`
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
CreatedTs *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=created_ts,json=createdTs,proto3" json:"created_ts,omitempty"`
Filename string `protobuf:"bytes,3,opt,name=filename,proto3" json:"filename,omitempty"`
ExternalLink string `protobuf:"bytes,4,opt,name=external_link,json=externalLink,proto3" json:"external_link,omitempty"`
Type string `protobuf:"bytes,5,opt,name=type,proto3" json:"type,omitempty"`
Size int64 `protobuf:"varint,6,opt,name=size,proto3" json:"size,omitempty"`
MemoId *int32 `protobuf:"varint,7,opt,name=memo_id,json=memoId,proto3,oneof" json:"memo_id,omitempty"`
}
func (x *Resource) Reset() {
@ -110,9 +110,9 @@ func (x *Resource) GetSize() int64 {
return 0
}
func (x *Resource) GetRelatedMemoId() int32 {
if x != nil && x.RelatedMemoId != nil {
return *x.RelatedMemoId
func (x *Resource) GetMemoId() int32 {
if x != nil && x.MemoId != nil {
return *x.MemoId
}
return 0
}
@ -211,7 +211,7 @@ var file_api_v2_resource_service_proto_rawDesc = []byte{
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d,
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xff, 0x01, 0x0a,
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe8, 0x01, 0x0a,
0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x69, 0x64, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65,
0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
@ -223,38 +223,36 @@ var file_api_v2_resource_service_proto_rawDesc = []byte{
0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61,
0x6c, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a,
0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x2b, 0x0a,
0x0f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x5f, 0x69, 0x64,
0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x0d, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x65,
0x64, 0x4d, 0x65, 0x6d, 0x6f, 0x49, 0x64, 0x88, 0x01, 0x01, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x72,
0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x5f, 0x69, 0x64, 0x22, 0x16,
0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x4d, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x34, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76,
0x32, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x73, 0x32, 0x86, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x73, 0x0a, 0x0d, 0x4c, 0x69, 0x73,
0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x22, 0x2e, 0x6d, 0x65, 0x6d,
0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23,
0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69,
0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x12, 0x11, 0x2f, 0x61, 0x70,
0x69, 0x2f, 0x76, 0x32, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x42, 0xac,
0x01, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69,
0x2e, 0x76, 0x32, 0x42, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x73, 0x65, 0x6d, 0x65, 0x6d, 0x6f, 0x73,
0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e,
0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x3b, 0x61, 0x70, 0x69, 0x76, 0x32, 0xa2, 0x02, 0x03,
0x4d, 0x41, 0x58, 0xaa, 0x02, 0x0c, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x70, 0x69, 0x2e,
0x56, 0x32, 0xca, 0x02, 0x0c, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56,
0x32, 0xe2, 0x02, 0x18, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x32,
0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0e, 0x4d,
0x65, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x41, 0x70, 0x69, 0x3a, 0x3a, 0x56, 0x32, 0x62, 0x06, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x33,
0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x1c, 0x0a,
0x07, 0x6d, 0x65, 0x6d, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00,
0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x49, 0x64, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f,
0x6d, 0x65, 0x6d, 0x6f, 0x5f, 0x69, 0x64, 0x22, 0x16, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x52,
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22,
0x4d, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x65,
0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x32, 0x86,
0x01, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69,
0x63, 0x65, 0x12, 0x73, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72,
0x63, 0x65, 0x73, 0x12, 0x22, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e,
0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e,
0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75,
0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3,
0xe4, 0x93, 0x02, 0x13, 0x12, 0x11, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x72, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x42, 0xac, 0x01, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e,
0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x42, 0x14, 0x52, 0x65,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f,
0x74, 0x6f, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x75, 0x73, 0x65, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32,
0x3b, 0x61, 0x70, 0x69, 0x76, 0x32, 0xa2, 0x02, 0x03, 0x4d, 0x41, 0x58, 0xaa, 0x02, 0x0c, 0x4d,
0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x70, 0x69, 0x2e, 0x56, 0x32, 0xca, 0x02, 0x0c, 0x4d, 0x65,
0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x32, 0xe2, 0x02, 0x18, 0x4d, 0x65, 0x6d,
0x6f, 0x73, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x32, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74,
0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0e, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x41,
0x70, 0x69, 0x3a, 0x3a, 0x56, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (

View file

@ -92,6 +92,7 @@ func (t *TelegramHandler) MessageHandle(ctx context.Context, bot *telegram.Bot,
Filename: attachment.FileName,
Type: attachment.GetMimeType(),
Size: attachment.FileSize,
MemoID: &memoMessage.ID,
}
err := apiv1.SaveResourceBlob(ctx, t.store, &create, bytes.NewReader(attachment.Data))
@ -100,20 +101,11 @@ func (t *TelegramHandler) MessageHandle(ctx context.Context, bot *telegram.Bot,
return err
}
resource, err := t.store.CreateResource(ctx, &create)
_, err = t.store.CreateResource(ctx, &create)
if err != nil {
_, err := bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("Failed to CreateResource: %s", err), nil)
return err
}
_, err = t.store.UpsertMemoResource(ctx, &store.UpsertMemoResource{
MemoID: memoMessage.ID,
ResourceID: resource.ID,
})
if err != nil {
_, err := bot.EditMessage(ctx, message.Chat.ID, reply.MessageID, fmt.Sprintf("Failed to UpsertMemoResource: %s", err), nil)
return err
}
}
keyboard := generateKeyboardForMemoID(memoMessage.ID)

View file

@ -81,19 +81,13 @@ CREATE TABLE resource (
external_link TEXT NOT NULL DEFAULT '',
type TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0,
internal_path TEXT NOT NULL DEFAULT ''
internal_path TEXT NOT NULL DEFAULT '',
memo_id INTEGER
);
CREATE INDEX idx_resource_creator_id ON resource (creator_id);
-- memo_resource
CREATE TABLE memo_resource (
memo_id INTEGER NOT NULL,
resource_id INTEGER NOT NULL,
created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')),
UNIQUE(memo_id, resource_id)
);
CREATE INDEX idx_resource_memo_id ON resource (memo_id);
-- tag
CREATE TABLE tag (

View file

@ -0,0 +1,13 @@
ALTER TABLE resource ADD COLUMN memo_id INTEGER;
UPDATE resource
SET memo_id = (
SELECT memo_id
FROM memo_resource
WHERE resource.id = memo_resource.resource_id
LIMIT 1
);
DROP TABLE memo_resource;
CREATE INDEX idx_resource_memo_id ON resource (memo_id);

View file

@ -175,7 +175,7 @@ func (s *Store) ListMemos(ctx context.Context, find *FindMemo) ([]*Memo, error)
memo.content AS content,
memo.visibility AS visibility,
CASE WHEN memo_organizer.pinned = 1 THEN 1 ELSE 0 END AS pinned,
GROUP_CONCAT(memo_resource.resource_id) AS resource_id_list,
GROUP_CONCAT(resource.id) AS resource_id_list,
(
SELECT
GROUP_CONCAT(related_memo_id || ':' || type)
@ -191,7 +191,7 @@ func (s *Store) ListMemos(ctx context.Context, find *FindMemo) ([]*Memo, error)
LEFT JOIN
memo_organizer ON memo.id = memo_organizer.memo_id
LEFT JOIN
memo_resource ON memo.id = memo_resource.memo_id
resource ON memo.id = resource.memo_id
WHERE ` + strings.Join(where, " AND ") + `
GROUP BY memo.id
ORDER BY ` + strings.Join(orders, ", ") + `

View file

@ -1,168 +0,0 @@
package store
import (
"context"
"database/sql"
"strings"
)
type MemoResource struct {
MemoID int32
ResourceID int32
CreatedTs int64
UpdatedTs int64
}
type UpsertMemoResource struct {
MemoID int32
ResourceID int32
CreatedTs int64
UpdatedTs *int64
}
type FindMemoResource struct {
MemoID *int32
ResourceID *int32
}
type DeleteMemoResource struct {
MemoID *int32
ResourceID *int32
}
func (s *Store) UpsertMemoResource(ctx context.Context, upsert *UpsertMemoResource) (*MemoResource, error) {
set := []string{"memo_id", "resource_id"}
args := []any{upsert.MemoID, upsert.ResourceID}
placeholder := []string{"?", "?"}
if v := upsert.UpdatedTs; v != nil {
set, args, placeholder = append(set, "updated_ts"), append(args, v), append(placeholder, "?")
}
query := `
INSERT INTO memo_resource (
` + strings.Join(set, ", ") + `
)
VALUES (` + strings.Join(placeholder, ",") + `)
ON CONFLICT(memo_id, resource_id) DO UPDATE
SET
updated_ts = EXCLUDED.updated_ts
RETURNING memo_id, resource_id, created_ts, updated_ts
`
memoResource := &MemoResource{}
if err := s.db.QueryRowContext(ctx, query, args...).Scan(
&memoResource.MemoID,
&memoResource.ResourceID,
&memoResource.CreatedTs,
&memoResource.UpdatedTs,
); err != nil {
return nil, err
}
return memoResource, nil
}
func (s *Store) ListMemoResources(ctx context.Context, 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 := s.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, 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, err
}
list = append(list, &memoResource)
}
if err := rows.Err(); err != nil {
return nil, err
}
return list, nil
}
func (s *Store) GetMemoResource(ctx context.Context, find *FindMemoResource) (*MemoResource, error) {
list, err := s.ListMemoResources(ctx, find)
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, nil
}
memoResource := list[0]
return memoResource, nil
}
func (s *Store) DeleteMemoResource(ctx context.Context, delete *DeleteMemoResource) error {
where, args := []string{}, []any{}
if v := delete.MemoID; v != nil {
where, args = append(where, "memo_id = ?"), append(args, *v)
}
if v := delete.ResourceID; v != nil {
where, args = append(where, "resource_id = ?"), append(args, *v)
}
stmt := `DELETE FROM memo_resource WHERE ` + strings.Join(where, " AND ")
result, err := s.db.ExecContext(ctx, stmt, args...)
if err != nil {
return err
}
if _, err = result.RowsAffected(); err != nil {
return err
}
return nil
}
func vacuumMemoResource(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
memo_resource
WHERE
memo_id NOT IN (
SELECT
id
FROM
memo
)
OR resource_id NOT IN (
SELECT
id
FROM
resource
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return err
}
return nil
}

View file

@ -20,9 +20,7 @@ type Resource struct {
ExternalLink string
Type string
Size int64
// Related fields
RelatedMemoID *int32
MemoID *int32
}
type FindResource struct {
@ -41,11 +39,14 @@ type UpdateResource struct {
UpdatedTs *int64
Filename *string
InternalPath *string
MemoID *int32
UnbindMemo bool
Blob []byte
}
type DeleteResource struct {
ID int32
ID int32
MemoID *int32
}
func (s *Store) CreateResource(ctx context.Context, create *Resource) (*Resource, error) {

View file

@ -4,7 +4,6 @@ import (
"context"
"database/sql"
"fmt"
"strconv"
"strings"
"github.com/usememos/memos/store"
@ -45,35 +44,33 @@ func (d *Driver) ListResources(ctx context.Context, find *store.FindResource) ([
where, args := []string{"1 = 1"}, []any{}
if v := find.ID; v != nil {
where, args = append(where, "resource.id = ?"), append(args, *v)
where, args = append(where, "id = ?"), append(args, *v)
}
if v := find.CreatorID; v != nil {
where, args = append(where, "resource.creator_id = ?"), append(args, *v)
where, args = append(where, "creator_id = ?"), append(args, *v)
}
if v := find.Filename; v != nil {
where, args = append(where, "resource.filename = ?"), append(args, *v)
where, args = append(where, "filename = ?"), append(args, *v)
}
if v := find.MemoID; v != nil {
where, args = append(where, "resource.id in (SELECT resource_id FROM memo_resource WHERE memo_id = ?)"), append(args, *v)
where, args = append(where, "memo_id = ?"), append(args, *v)
}
if find.HasRelatedMemo {
where = append(where, "memo_resource.memo_id IS NOT NULL")
where = append(where, "memo_id IS NOT NULL")
}
fields := []string{"resource.id", "resource.filename", "resource.external_link", "resource.type", "resource.size", "resource.creator_id", "resource.created_ts", "resource.updated_ts", "internal_path"}
fields := []string{"id", "filename", "external_link", "type", "size", "creator_id", "created_ts", "updated_ts", "internal_path", "memo_id"}
if find.GetBlob {
fields = append(fields, "resource.blob")
fields = append(fields, "blob")
}
query := fmt.Sprintf(`
SELECT
GROUP_CONCAT(memo_resource.memo_id) as related_memo_ids,
%s
FROM resource
LEFT JOIN memo_resource ON resource.id = memo_resource.resource_id
WHERE %s
GROUP BY resource.id
ORDER BY resource.created_ts DESC
GROUP BY id
ORDER BY created_ts DESC
`, strings.Join(fields, ", "), strings.Join(where, " AND "))
if find.Limit != nil {
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)
@ -91,9 +88,8 @@ func (d *Driver) ListResources(ctx context.Context, find *store.FindResource) ([
list := make([]*store.Resource, 0)
for rows.Next() {
resource := store.Resource{}
var relatedMemoIDs sql.NullString
var memoID sql.NullInt32
dests := []any{
&relatedMemoIDs,
&resource.ID,
&resource.Filename,
&resource.ExternalLink,
@ -103,6 +99,7 @@ func (d *Driver) ListResources(ctx context.Context, find *store.FindResource) ([
&resource.CreatedTs,
&resource.UpdatedTs,
&resource.InternalPath,
&memoID,
}
if find.GetBlob {
dests = append(dests, &resource.Blob)
@ -110,17 +107,8 @@ func (d *Driver) ListResources(ctx context.Context, find *store.FindResource) ([
if err := rows.Scan(dests...); err != nil {
return nil, err
}
if relatedMemoIDs.Valid {
relatedMemoIDList := strings.Split(relatedMemoIDs.String, ",")
if len(relatedMemoIDList) > 0 {
// Only take the first related memo ID.
relatedMemoIDInt, err := strconv.ParseInt(relatedMemoIDList[0], 10, 32)
if err != nil {
return nil, err
}
relatedMemoID := int32(relatedMemoIDInt)
resource.RelatedMemoID = &relatedMemoID
}
if memoID.Valid {
resource.MemoID = &memoID.Int32
}
list = append(list, &resource)
}
@ -144,6 +132,12 @@ func (d *Driver) UpdateResource(ctx context.Context, update *store.UpdateResourc
if v := update.InternalPath; v != nil {
set, args = append(set, "internal_path = ?"), append(args, *v)
}
if v := update.MemoID; v != nil {
set, args = append(set, "memo_id = ?"), append(args, *v)
}
if update.UnbindMemo {
set = append(set, "memo_id = NULL")
}
if v := update.Blob; v != nil {
set, args = append(set, "blob = ?"), append(args, v)
}

View file

@ -109,9 +109,6 @@ func (*Store) vacuumImpl(ctx context.Context, tx *sql.Tx) error {
if err := vacuumMemoOrganizer(ctx, tx); err != nil {
return err
}
if err := vacuumMemoResource(ctx, tx); err != nil {
return err
}
if err := vacuumMemoRelations(ctx, tx); err != nil {
return err
}

View file

@ -31,17 +31,6 @@ func TestResourceStore(t *testing.T) {
require.NoError(t, err)
require.Equal(t, correctFilename, resource.Filename)
require.Equal(t, int32(1), resource.ID)
_, err = ts.UpsertMemoResource(ctx, &store.UpsertMemoResource{
MemoID: 1,
ResourceID: resource.ID,
})
require.NoError(t, err)
resource, err = ts.GetResource(ctx, &store.FindResource{
ID: &resource.ID,
})
require.NoError(t, err)
require.Equal(t, *resource.RelatedMemoID, int32(1))
notFoundResource, err := ts.GetResource(ctx, &store.FindResource{
Filename: &incorrectFilename,

View file

@ -3,7 +3,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import useLocalStorage from "react-use/lib/useLocalStorage";
import { upsertMemoResource } from "@/helpers/api";
import { TAB_SPACE_WIDTH, UNKNOWN_ID } from "@/helpers/consts";
import { clearContentQueryParam } from "@/helpers/utils";
import { getMatchedNodes } from "@/labs/marked";
@ -223,7 +222,10 @@ const MemoEditor = (props: Props) => {
if (resource) {
uploadedResourceList.push(resource);
if (memoId) {
await upsertMemoResource(memoId, resource.id);
await resourceStore.patchResource({
id: resource.id,
memoId,
});
}
}
}

View file

@ -155,20 +155,6 @@ export function deleteResourceById(id: ResourceId) {
return axios.delete(`/api/v1/resource/${id}`);
}
export function getMemoResourceList(memoId: MemoId) {
return axios.get<Resource[]>(`/api/v1/memo/${memoId}/resource`);
}
export function upsertMemoResource(memoId: MemoId, resourceId: ResourceId) {
return axios.post(`/api/v1/memo/${memoId}/resource`, {
resourceId,
});
}
export function deleteMemoResource(memoId: MemoId, resourceId: ResourceId) {
return axios.delete(`/api/v1/memo/${memoId}/resource/${resourceId}`);
}
export function getTagList() {
return axios.get<string[]>(`/api/v1/tag`);
}

View file

@ -40,31 +40,14 @@ export const useResourceStore = () => {
store.dispatch(setResources([resource, ...resourceList]));
return resource;
},
async createResourcesWithBlob(files: FileList): Promise<Array<Resource>> {
let newResourceList: Array<Resource> = [];
for (const file of files) {
const { name: filename, size } = file;
if (size > maxUploadSizeMiB * 1024 * 1024) {
return Promise.reject(t("message.file-exceeds-upload-limit-of", { file: filename, size: maxUploadSizeMiB }));
}
const formData = new FormData();
formData.append("file", file, filename);
const { data: resource } = await api.createResourceWithBlob(formData);
newResourceList = [resource, ...newResourceList];
}
const resourceList = state.resources;
store.dispatch(setResources([...newResourceList, ...resourceList]));
return newResourceList;
},
async deleteResourceById(id: ResourceId) {
await api.deleteResourceById(id);
store.dispatch(deleteResource(id));
},
async patchResource(resourcePatch: ResourcePatch): Promise<Resource> {
const { data: resource } = await api.patchResource(resourcePatch);
store.dispatch(patchResource(resource));
return resource;
},
async deleteResourceById(id: ResourceId) {
await api.deleteResourceById(id);
store.dispatch(deleteResource(id));
},
};
};

View file

@ -9,6 +9,7 @@ interface ResourceCreate {
interface ResourcePatch {
id: ResourceId;
filename?: string;
memoId?: number;
}
interface ResourceFind {