mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
chore: retire memo resource relation table
This commit is contained in:
@@ -319,9 +319,9 @@ func (s *APIV1Service) CreateMemo(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, resourceID := range createMemoRequest.ResourceIDList {
|
for _, resourceID := range createMemoRequest.ResourceIDList {
|
||||||
if _, err := s.Store.UpsertMemoResource(ctx, &store.UpsertMemoResource{
|
if _, err := s.Store.UpdateResource(ctx, &store.UpdateResource{
|
||||||
MemoID: memo.ID,
|
ID: resourceID,
|
||||||
ResourceID: resourceID,
|
MemoID: &memo.ID,
|
||||||
}); err != nil {
|
}); 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)
|
||||||
}
|
}
|
||||||
@@ -694,19 +694,18 @@ func (s *APIV1Service) UpdateMemo(c echo.Context) error {
|
|||||||
if patchMemoRequest.ResourceIDList != nil {
|
if patchMemoRequest.ResourceIDList != nil {
|
||||||
addedResourceIDList, removedResourceIDList := getIDListDiff(memo.ResourceIDList, patchMemoRequest.ResourceIDList)
|
addedResourceIDList, removedResourceIDList := getIDListDiff(memo.ResourceIDList, patchMemoRequest.ResourceIDList)
|
||||||
for _, resourceID := range addedResourceIDList {
|
for _, resourceID := range addedResourceIDList {
|
||||||
if _, err := s.Store.UpsertMemoResource(ctx, &store.UpsertMemoResource{
|
if _, err := s.Store.UpdateResource(ctx, &store.UpdateResource{
|
||||||
MemoID: memo.ID,
|
ID: resourceID,
|
||||||
ResourceID: resourceID,
|
MemoID: &memo.ID,
|
||||||
}); err != nil {
|
}); 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, resourceID := range removedResourceIDList {
|
for _, resourceID := range removedResourceIDList {
|
||||||
if err := s.Store.DeleteMemoResource(ctx, &store.DeleteMemoResource{
|
if err := s.Store.DeleteResource(ctx, &store.DeleteResource{
|
||||||
MemoID: &memo.ID,
|
ID: resourceID,
|
||||||
ResourceID: &resourceID,
|
|
||||||
}); err != nil {
|
}); 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)
|
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{
|
resource, err := s.Store.GetResource(ctx, &store.FindResource{
|
||||||
ID: &resourceID,
|
ID: &resourceID,
|
||||||
GetBlob: true,
|
GetBlob: true,
|
||||||
@@ -405,10 +394,20 @@ func (s *APIV1Service) streamResource(c echo.Context) error {
|
|||||||
if resource == nil {
|
if resource == nil {
|
||||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource not found: %d", resourceID))
|
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("Resource not found: %d", resourceID))
|
||||||
}
|
}
|
||||||
|
// Check the related memo visibility.
|
||||||
// Private resource require logined user is the creator
|
if resource.MemoID != nil {
|
||||||
if resourceVisibility == store.Private && (!ok || userID != resource.CreatorID) {
|
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, "Resource visibility not match").SetInternal(err)
|
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
|
blob := resource.Blob
|
||||||
@@ -542,47 +541,6 @@ func getOrGenerateThumbnailImage(srcBlob []byte, dstPath string) ([]byte, error)
|
|||||||
return dstBlob, nil
|
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 {
|
func convertResourceFromStore(resource *store.Resource) *Resource {
|
||||||
return &Resource{
|
return &Resource{
|
||||||
ID: resource.ID,
|
ID: resource.ID,
|
||||||
|
@@ -58,7 +58,6 @@ func (s *APIV1Service) Register(rootGroup *echo.Group) {
|
|||||||
s.registerResourceRoutes(apiV1Group)
|
s.registerResourceRoutes(apiV1Group)
|
||||||
s.registerMemoRoutes(apiV1Group)
|
s.registerMemoRoutes(apiV1Group)
|
||||||
s.registerMemoOrganizerRoutes(apiV1Group)
|
s.registerMemoOrganizerRoutes(apiV1Group)
|
||||||
s.registerMemoResourceRoutes(apiV1Group)
|
|
||||||
s.registerMemoRelationRoutes(apiV1Group)
|
s.registerMemoRelationRoutes(apiV1Group)
|
||||||
|
|
||||||
// Register public routes.
|
// Register public routes.
|
||||||
|
@@ -47,12 +47,12 @@ func (s *ResourceService) ListResources(ctx context.Context, _ *apiv2pb.ListReso
|
|||||||
|
|
||||||
func convertResourceFromStore(resource *store.Resource) *apiv2pb.Resource {
|
func convertResourceFromStore(resource *store.Resource) *apiv2pb.Resource {
|
||||||
return &apiv2pb.Resource{
|
return &apiv2pb.Resource{
|
||||||
Id: resource.ID,
|
Id: resource.ID,
|
||||||
CreatedTs: timestamppb.New(time.Unix(resource.CreatedTs, 0)),
|
CreatedTs: timestamppb.New(time.Unix(resource.CreatedTs, 0)),
|
||||||
Filename: resource.Filename,
|
Filename: resource.Filename,
|
||||||
ExternalLink: resource.ExternalLink,
|
ExternalLink: resource.ExternalLink,
|
||||||
Type: resource.Type,
|
Type: resource.Type,
|
||||||
Size: resource.Size,
|
Size: resource.Size,
|
||||||
RelatedMemoId: resource.RelatedMemoID,
|
MemoId: resource.MemoID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -20,7 +20,7 @@ message Resource {
|
|||||||
string external_link = 4;
|
string external_link = 4;
|
||||||
string type = 5;
|
string type = 5;
|
||||||
int64 size = 6;
|
int64 size = 6;
|
||||||
optional int32 related_memo_id = 7;
|
optional int32 memo_id = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ListResourcesRequest {}
|
message ListResourcesRequest {}
|
||||||
|
@@ -262,7 +262,7 @@
|
|||||||
| external_link | [string](#string) | | |
|
| external_link | [string](#string) | | |
|
||||||
| type | [string](#string) | | |
|
| type | [string](#string) | | |
|
||||||
| size | [int64](#int64) | | |
|
| size | [int64](#int64) | | |
|
||||||
| related_memo_id | [int32](#int32) | optional | |
|
| memo_id | [int32](#int32) | optional | |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@@ -27,13 +27,13 @@ type Resource struct {
|
|||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"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"`
|
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"`
|
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"`
|
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"`
|
Type string `protobuf:"bytes,5,opt,name=type,proto3" json:"type,omitempty"`
|
||||||
Size int64 `protobuf:"varint,6,opt,name=size,proto3" json:"size,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"`
|
MemoId *int32 `protobuf:"varint,7,opt,name=memo_id,json=memoId,proto3,oneof" json:"memo_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Resource) Reset() {
|
func (x *Resource) Reset() {
|
||||||
@@ -110,9 +110,9 @@ func (x *Resource) GetSize() int64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Resource) GetRelatedMemoId() int32 {
|
func (x *Resource) GetMemoId() int32 {
|
||||||
if x != nil && x.RelatedMemoId != nil {
|
if x != nil && x.MemoId != nil {
|
||||||
return *x.RelatedMemoId
|
return *x.MemoId
|
||||||
}
|
}
|
||||||
return 0
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
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,
|
0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x1c, 0x0a,
|
||||||
0x0f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x5f, 0x69, 0x64,
|
0x07, 0x6d, 0x65, 0x6d, 0x6f, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00,
|
||||||
0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x0d, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x65,
|
0x52, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x49, 0x64, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f,
|
||||||
0x64, 0x4d, 0x65, 0x6d, 0x6f, 0x49, 0x64, 0x88, 0x01, 0x01, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x72,
|
0x6d, 0x65, 0x6d, 0x6f, 0x5f, 0x69, 0x64, 0x22, 0x16, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x52,
|
||||||
0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x6d, 0x6f, 0x5f, 0x69, 0x64, 0x22, 0x16,
|
0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22,
|
||||||
0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52,
|
0x4d, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73,
|
||||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x4d, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65,
|
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f,
|
||||||
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x65,
|
||||||
0x34, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
|
0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75,
|
||||||
0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76,
|
0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x32, 0x86,
|
||||||
0x32, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f,
|
0x01, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69,
|
||||||
0x75, 0x72, 0x63, 0x65, 0x73, 0x32, 0x86, 0x01, 0x0a, 0x0f, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72,
|
0x63, 0x65, 0x12, 0x73, 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72,
|
||||||
0x63, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x73, 0x0a, 0x0d, 0x4c, 0x69, 0x73,
|
0x63, 0x65, 0x73, 0x12, 0x22, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e,
|
||||||
0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x22, 0x2e, 0x6d, 0x65, 0x6d,
|
0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73,
|
||||||
0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65,
|
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e,
|
||||||
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23,
|
0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75,
|
||||||
0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x2e, 0x4c, 0x69,
|
0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3,
|
||||||
0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
|
0xe4, 0x93, 0x02, 0x13, 0x12, 0x11, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x2f, 0x72, 0x65,
|
||||||
0x6e, 0x73, 0x65, 0x22, 0x19, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x13, 0x12, 0x11, 0x2f, 0x61, 0x70,
|
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x42, 0xac, 0x01, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e,
|
||||||
0x69, 0x2f, 0x76, 0x32, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x42, 0xac,
|
0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x32, 0x42, 0x14, 0x52, 0x65,
|
||||||
0x01, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x2e, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x61, 0x70, 0x69,
|
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f,
|
||||||
0x2e, 0x76, 0x32, 0x42, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x72,
|
0x74, 0x6f, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
|
||||||
0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74,
|
0x2f, 0x75, 0x73, 0x65, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f,
|
||||||
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x75, 0x73, 0x65, 0x6d, 0x65, 0x6d, 0x6f, 0x73,
|
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32,
|
||||||
0x2f, 0x6d, 0x65, 0x6d, 0x6f, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x65, 0x6e,
|
0x3b, 0x61, 0x70, 0x69, 0x76, 0x32, 0xa2, 0x02, 0x03, 0x4d, 0x41, 0x58, 0xaa, 0x02, 0x0c, 0x4d,
|
||||||
0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x32, 0x3b, 0x61, 0x70, 0x69, 0x76, 0x32, 0xa2, 0x02, 0x03,
|
0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x70, 0x69, 0x2e, 0x56, 0x32, 0xca, 0x02, 0x0c, 0x4d, 0x65,
|
||||||
0x4d, 0x41, 0x58, 0xaa, 0x02, 0x0c, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x2e, 0x41, 0x70, 0x69, 0x2e,
|
0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x32, 0xe2, 0x02, 0x18, 0x4d, 0x65, 0x6d,
|
||||||
0x56, 0x32, 0xca, 0x02, 0x0c, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56,
|
0x6f, 0x73, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x32, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74,
|
||||||
0x32, 0xe2, 0x02, 0x18, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x5c, 0x41, 0x70, 0x69, 0x5c, 0x56, 0x32,
|
0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0e, 0x4d, 0x65, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x41,
|
||||||
0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0e, 0x4d,
|
0x70, 0x69, 0x3a, 0x3a, 0x56, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
0x65, 0x6d, 0x6f, 0x73, 0x3a, 0x3a, 0x41, 0x70, 0x69, 0x3a, 0x3a, 0x56, 0x32, 0x62, 0x06, 0x70,
|
|
||||||
0x72, 0x6f, 0x74, 0x6f, 0x33,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@@ -92,6 +92,7 @@ func (t *TelegramHandler) MessageHandle(ctx context.Context, bot *telegram.Bot,
|
|||||||
Filename: attachment.FileName,
|
Filename: attachment.FileName,
|
||||||
Type: attachment.GetMimeType(),
|
Type: attachment.GetMimeType(),
|
||||||
Size: attachment.FileSize,
|
Size: attachment.FileSize,
|
||||||
|
MemoID: &memoMessage.ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := apiv1.SaveResourceBlob(ctx, t.store, &create, bytes.NewReader(attachment.Data))
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resource, err := t.store.CreateResource(ctx, &create)
|
_, err = t.store.CreateResource(ctx, &create)
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
_, 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)
|
keyboard := generateKeyboardForMemoID(memoMessage.ID)
|
||||||
|
@@ -81,19 +81,13 @@ CREATE TABLE resource (
|
|||||||
external_link TEXT NOT NULL DEFAULT '',
|
external_link TEXT NOT NULL DEFAULT '',
|
||||||
type TEXT NOT NULL DEFAULT '',
|
type TEXT NOT NULL DEFAULT '',
|
||||||
size INTEGER NOT NULL DEFAULT 0,
|
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);
|
CREATE INDEX idx_resource_creator_id ON resource (creator_id);
|
||||||
|
|
||||||
-- memo_resource
|
CREATE INDEX idx_resource_memo_id ON resource (memo_id);
|
||||||
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)
|
|
||||||
);
|
|
||||||
|
|
||||||
-- tag
|
-- tag
|
||||||
CREATE TABLE 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.content AS content,
|
||||||
memo.visibility AS visibility,
|
memo.visibility AS visibility,
|
||||||
CASE WHEN memo_organizer.pinned = 1 THEN 1 ELSE 0 END AS pinned,
|
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
|
SELECT
|
||||||
GROUP_CONCAT(related_memo_id || ':' || type)
|
GROUP_CONCAT(related_memo_id || ':' || type)
|
||||||
@@ -191,7 +191,7 @@ func (s *Store) ListMemos(ctx context.Context, find *FindMemo) ([]*Memo, error)
|
|||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
memo_organizer ON memo.id = memo_organizer.memo_id
|
memo_organizer ON memo.id = memo_organizer.memo_id
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
memo_resource ON memo.id = memo_resource.memo_id
|
resource ON memo.id = resource.memo_id
|
||||||
WHERE ` + strings.Join(where, " AND ") + `
|
WHERE ` + strings.Join(where, " AND ") + `
|
||||||
GROUP BY memo.id
|
GROUP BY memo.id
|
||||||
ORDER BY ` + strings.Join(orders, ", ") + `
|
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
|
ExternalLink string
|
||||||
Type string
|
Type string
|
||||||
Size int64
|
Size int64
|
||||||
|
MemoID *int32
|
||||||
// Related fields
|
|
||||||
RelatedMemoID *int32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type FindResource struct {
|
type FindResource struct {
|
||||||
@@ -41,11 +39,14 @@ type UpdateResource struct {
|
|||||||
UpdatedTs *int64
|
UpdatedTs *int64
|
||||||
Filename *string
|
Filename *string
|
||||||
InternalPath *string
|
InternalPath *string
|
||||||
|
MemoID *int32
|
||||||
|
UnbindMemo bool
|
||||||
Blob []byte
|
Blob []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeleteResource struct {
|
type DeleteResource struct {
|
||||||
ID int32
|
ID int32
|
||||||
|
MemoID *int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) CreateResource(ctx context.Context, create *Resource) (*Resource, error) {
|
func (s *Store) CreateResource(ctx context.Context, create *Resource) (*Resource, error) {
|
||||||
|
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/usememos/memos/store"
|
"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{}
|
where, args := []string{"1 = 1"}, []any{}
|
||||||
|
|
||||||
if v := find.ID; v != nil {
|
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 {
|
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 {
|
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 {
|
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 {
|
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 {
|
if find.GetBlob {
|
||||||
fields = append(fields, "resource.blob")
|
fields = append(fields, "blob")
|
||||||
}
|
}
|
||||||
|
|
||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
SELECT
|
SELECT
|
||||||
GROUP_CONCAT(memo_resource.memo_id) as related_memo_ids,
|
|
||||||
%s
|
%s
|
||||||
FROM resource
|
FROM resource
|
||||||
LEFT JOIN memo_resource ON resource.id = memo_resource.resource_id
|
|
||||||
WHERE %s
|
WHERE %s
|
||||||
GROUP BY resource.id
|
GROUP BY id
|
||||||
ORDER BY resource.created_ts DESC
|
ORDER BY created_ts DESC
|
||||||
`, strings.Join(fields, ", "), strings.Join(where, " AND "))
|
`, strings.Join(fields, ", "), strings.Join(where, " AND "))
|
||||||
if find.Limit != nil {
|
if find.Limit != nil {
|
||||||
query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit)
|
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)
|
list := make([]*store.Resource, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
resource := store.Resource{}
|
resource := store.Resource{}
|
||||||
var relatedMemoIDs sql.NullString
|
var memoID sql.NullInt32
|
||||||
dests := []any{
|
dests := []any{
|
||||||
&relatedMemoIDs,
|
|
||||||
&resource.ID,
|
&resource.ID,
|
||||||
&resource.Filename,
|
&resource.Filename,
|
||||||
&resource.ExternalLink,
|
&resource.ExternalLink,
|
||||||
@@ -103,6 +99,7 @@ func (d *Driver) ListResources(ctx context.Context, find *store.FindResource) ([
|
|||||||
&resource.CreatedTs,
|
&resource.CreatedTs,
|
||||||
&resource.UpdatedTs,
|
&resource.UpdatedTs,
|
||||||
&resource.InternalPath,
|
&resource.InternalPath,
|
||||||
|
&memoID,
|
||||||
}
|
}
|
||||||
if find.GetBlob {
|
if find.GetBlob {
|
||||||
dests = append(dests, &resource.Blob)
|
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 {
|
if err := rows.Scan(dests...); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if relatedMemoIDs.Valid {
|
if memoID.Valid {
|
||||||
relatedMemoIDList := strings.Split(relatedMemoIDs.String, ",")
|
resource.MemoID = &memoID.Int32
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
list = append(list, &resource)
|
list = append(list, &resource)
|
||||||
}
|
}
|
||||||
@@ -144,6 +132,12 @@ func (d *Driver) UpdateResource(ctx context.Context, update *store.UpdateResourc
|
|||||||
if v := update.InternalPath; v != nil {
|
if v := update.InternalPath; v != nil {
|
||||||
set, args = append(set, "internal_path = ?"), append(args, *v)
|
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 {
|
if v := update.Blob; v != nil {
|
||||||
set, args = append(set, "blob = ?"), append(args, v)
|
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 {
|
if err := vacuumMemoOrganizer(ctx, tx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := vacuumMemoResource(ctx, tx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := vacuumMemoRelations(ctx, tx); err != nil {
|
if err := vacuumMemoRelations(ctx, tx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -31,17 +31,6 @@ func TestResourceStore(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, correctFilename, resource.Filename)
|
require.Equal(t, correctFilename, resource.Filename)
|
||||||
require.Equal(t, int32(1), resource.ID)
|
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{
|
notFoundResource, err := ts.GetResource(ctx, &store.FindResource{
|
||||||
Filename: &incorrectFilename,
|
Filename: &incorrectFilename,
|
||||||
|
@@ -3,7 +3,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import useLocalStorage from "react-use/lib/useLocalStorage";
|
import useLocalStorage from "react-use/lib/useLocalStorage";
|
||||||
import { upsertMemoResource } from "@/helpers/api";
|
|
||||||
import { TAB_SPACE_WIDTH, UNKNOWN_ID } from "@/helpers/consts";
|
import { TAB_SPACE_WIDTH, UNKNOWN_ID } from "@/helpers/consts";
|
||||||
import { clearContentQueryParam } from "@/helpers/utils";
|
import { clearContentQueryParam } from "@/helpers/utils";
|
||||||
import { getMatchedNodes } from "@/labs/marked";
|
import { getMatchedNodes } from "@/labs/marked";
|
||||||
@@ -223,7 +222,10 @@ const MemoEditor = (props: Props) => {
|
|||||||
if (resource) {
|
if (resource) {
|
||||||
uploadedResourceList.push(resource);
|
uploadedResourceList.push(resource);
|
||||||
if (memoId) {
|
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}`);
|
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() {
|
export function getTagList() {
|
||||||
return axios.get<string[]>(`/api/v1/tag`);
|
return axios.get<string[]>(`/api/v1/tag`);
|
||||||
}
|
}
|
||||||
|
@@ -40,31 +40,14 @@ export const useResourceStore = () => {
|
|||||||
store.dispatch(setResources([resource, ...resourceList]));
|
store.dispatch(setResources([resource, ...resourceList]));
|
||||||
return resource;
|
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> {
|
async patchResource(resourcePatch: ResourcePatch): Promise<Resource> {
|
||||||
const { data: resource } = await api.patchResource(resourcePatch);
|
const { data: resource } = await api.patchResource(resourcePatch);
|
||||||
store.dispatch(patchResource(resource));
|
store.dispatch(patchResource(resource));
|
||||||
return 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 {
|
interface ResourcePatch {
|
||||||
id: ResourceId;
|
id: ResourceId;
|
||||||
filename?: string;
|
filename?: string;
|
||||||
|
memoId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResourceFind {
|
interface ResourceFind {
|
||||||
|
Reference in New Issue
Block a user