mirror of
https://github.com/usememos/memos.git
synced 2024-09-20 14:35:54 +08:00
chore: retire memo resource relation table
This commit is contained in:
parent
4f10198ec0
commit
6007f48b7d
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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 | |
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 (
|
||||
|
|
13
store/db/migration/prod/0.16/00__add_memo_id_to_resource.sql
Normal file
13
store/db/migration/prod/0.16/00__add_memo_id_to_resource.sql
Normal 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);
|
|
@ -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, ", ") + `
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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`);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
1
web/src/types/modules/resource.d.ts
vendored
1
web/src/types/modules/resource.d.ts
vendored
|
@ -9,6 +9,7 @@ interface ResourceCreate {
|
|||
interface ResourcePatch {
|
||||
id: ResourceId;
|
||||
filename?: string;
|
||||
memoId?: number;
|
||||
}
|
||||
|
||||
interface ResourceFind {
|
||||
|
|
Loading…
Reference in a new issue