Commit a4920d46 authored by Steven's avatar Steven

refactor: attachment service part2

parent bb5809ca
// Package httpgetter is using to get resources from url.
// * Get metadata for website;
// * Get image blob to avoid CORS;
package httpgetter package httpgetter
...@@ -7,17 +7,17 @@ import "store/workspace_setting.proto"; ...@@ -7,17 +7,17 @@ import "store/workspace_setting.proto";
option go_package = "gen/store"; option go_package = "gen/store";
enum ResourceStorageType { enum AttachmentStorageType {
RESOURCE_STORAGE_TYPE_UNSPECIFIED = 0; ATTACHMENT_STORAGE_TYPE_UNSPECIFIED = 0;
// Resource is stored locally. AKA, local file system. // Attachment is stored locally. AKA, local file system.
LOCAL = 1; LOCAL = 1;
// Resource is stored in S3. // Attachment is stored in S3.
S3 = 2; S3 = 2;
// Resource is stored in an external storage. The reference is a URL. // Attachment is stored in an external storage. The reference is a URL.
EXTERNAL = 3; EXTERNAL = 3;
} }
message ResourcePayload { message AttachmentPayload {
oneof payload { oneof payload {
S3Object s3_object = 1; S3Object s3_object = 1;
} }
......
...@@ -18,7 +18,7 @@ var authenticationAllowlistMethods = map[string]bool{ ...@@ -18,7 +18,7 @@ var authenticationAllowlistMethods = map[string]bool{
"/memos.api.v1.MemoService/GetMemo": true, "/memos.api.v1.MemoService/GetMemo": true,
"/memos.api.v1.MemoService/ListMemos": true, "/memos.api.v1.MemoService/ListMemos": true,
"/memos.api.v1.MarkdownService/GetLinkMetadata": true, "/memos.api.v1.MarkdownService/GetLinkMetadata": true,
"/memos.api.v1.ResourceService/GetResourceBinary": true, "/memos.api.v1.AttachmentService/GetAttachmentBinary": true,
} }
// isUnauthorizeAllowedMethod returns whether the method is exempted from authentication. // isUnauthorizeAllowedMethod returns whether the method is exempted from authentication.
......
This diff is collapsed.
...@@ -22,54 +22,54 @@ func (s *APIV1Service) SetMemoAttachments(ctx context.Context, request *v1pb.Set ...@@ -22,54 +22,54 @@ func (s *APIV1Service) SetMemoAttachments(ctx context.Context, request *v1pb.Set
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo") return nil, status.Errorf(codes.Internal, "failed to get memo")
} }
resources, err := s.Store.ListResources(ctx, &store.FindResource{ attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{
MemoID: &memo.ID, MemoID: &memo.ID,
}) })
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list resources") return nil, status.Errorf(codes.Internal, "failed to list attachments")
} }
// Delete resources that are not in the request. // Delete attachments that are not in the request.
for _, resource := range resources { for _, attachment := range attachments {
found := false found := false
for _, requestResource := range request.Attachments { for _, requestAttachment := range request.Attachments {
requestResourceUID, err := ExtractAttachmentUIDFromName(requestResource.Name) requestAttachmentUID, err := ExtractAttachmentUIDFromName(requestAttachment.Name)
if err != nil { if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid attachment name: %v", err) return nil, status.Errorf(codes.InvalidArgument, "invalid attachment name: %v", err)
} }
if resource.UID == requestResourceUID { if attachment.UID == requestAttachmentUID {
found = true found = true
break break
} }
} }
if !found { if !found {
if err = s.Store.DeleteResource(ctx, &store.DeleteResource{ if err = s.Store.DeleteAttachment(ctx, &store.DeleteAttachment{
ID: int32(resource.ID), ID: int32(attachment.ID),
MemoID: &memo.ID, MemoID: &memo.ID,
}); err != nil { }); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete resource") return nil, status.Errorf(codes.Internal, "failed to delete attachment")
} }
} }
} }
slices.Reverse(request.Attachments) slices.Reverse(request.Attachments)
// Update resources' memo_id in the request. // Update attachments' memo_id in the request.
for index, resource := range request.Attachments { for index, attachment := range request.Attachments {
resourceUID, err := ExtractAttachmentUIDFromName(resource.Name) attachmentUID, err := ExtractAttachmentUIDFromName(attachment.Name)
if err != nil { if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid attachment name: %v", err) return nil, status.Errorf(codes.InvalidArgument, "invalid attachment name: %v", err)
} }
tempResource, err := s.Store.GetResource(ctx, &store.FindResource{UID: &resourceUID}) tempAttachment, err := s.Store.GetAttachment(ctx, &store.FindAttachment{UID: &attachmentUID})
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get resource: %v", err) return nil, status.Errorf(codes.Internal, "failed to get attachment: %v", err)
} }
updatedTs := time.Now().Unix() + int64(index) updatedTs := time.Now().Unix() + int64(index)
if err := s.Store.UpdateResource(ctx, &store.UpdateResource{ if err := s.Store.UpdateAttachment(ctx, &store.UpdateAttachment{
ID: tempResource.ID, ID: tempAttachment.ID,
MemoID: &memo.ID, MemoID: &memo.ID,
UpdatedTs: &updatedTs, UpdatedTs: &updatedTs,
}); err != nil { }); err != nil {
return nil, status.Errorf(codes.Internal, "failed to update resource: %v", err) return nil, status.Errorf(codes.Internal, "failed to update attachment: %v", err)
} }
} }
...@@ -85,18 +85,18 @@ func (s *APIV1Service) ListMemoAttachments(ctx context.Context, request *v1pb.Li ...@@ -85,18 +85,18 @@ func (s *APIV1Service) ListMemoAttachments(ctx context.Context, request *v1pb.Li
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo: %v", err) return nil, status.Errorf(codes.Internal, "failed to get memo: %v", err)
} }
resources, err := s.Store.ListResources(ctx, &store.FindResource{ attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{
MemoID: &memo.ID, MemoID: &memo.ID,
}) })
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list resources: %v", err) return nil, status.Errorf(codes.Internal, "failed to list attachments: %v", err)
} }
response := &v1pb.ListMemoAttachmentsResponse{ response := &v1pb.ListMemoAttachmentsResponse{
Attachments: []*v1pb.Attachment{}, Attachments: []*v1pb.Attachment{},
} }
for _, resource := range resources { for _, attachment := range attachments {
response.Attachments = append(response.Attachments, s.convertAttachmentFromStore(ctx, resource)) response.Attachments = append(response.Attachments, s.convertAttachmentFromStore(ctx, attachment))
} }
return response, nil return response, nil
} }
...@@ -399,14 +399,14 @@ func (s *APIV1Service) DeleteMemo(ctx context.Context, request *v1pb.DeleteMemoR ...@@ -399,14 +399,14 @@ func (s *APIV1Service) DeleteMemo(ctx context.Context, request *v1pb.DeleteMemoR
return nil, status.Errorf(codes.Internal, "failed to delete memo relations") return nil, status.Errorf(codes.Internal, "failed to delete memo relations")
} }
// Delete related resources. // Delete related attachments.
resources, err := s.Store.ListResources(ctx, &store.FindResource{MemoID: &memo.ID}) attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{MemoID: &memo.ID})
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to list resources") return nil, status.Errorf(codes.Internal, "failed to list attachments")
} }
for _, resource := range resources { for _, attachment := range attachments {
if err := s.Store.DeleteResource(ctx, &store.DeleteResource{ID: resource.ID}); err != nil { if err := s.Store.DeleteAttachment(ctx, &store.DeleteAttachment{ID: attachment.ID}); err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete resource") return nil, status.Errorf(codes.Internal, "failed to delete attachment")
} }
} }
......
...@@ -124,22 +124,22 @@ func (s *RSSService) generateRSSFromMemoList(ctx context.Context, memoList []*st ...@@ -124,22 +124,22 @@ func (s *RSSService) generateRSSFromMemoList(ctx context.Context, memoList []*st
Created: time.Unix(memo.CreatedTs, 0), Created: time.Unix(memo.CreatedTs, 0),
Id: link.Href, Id: link.Href,
} }
resources, err := s.Store.ListResources(ctx, &store.FindResource{ attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{
MemoID: &memo.ID, MemoID: &memo.ID,
}) })
if err != nil { if err != nil {
return "", err return "", err
} }
if len(resources) > 0 { if len(attachments) > 0 {
resource := resources[0] attachment := attachments[0]
enclosure := feeds.Enclosure{} enclosure := feeds.Enclosure{}
if resource.StorageType == storepb.ResourceStorageType_EXTERNAL || resource.StorageType == storepb.ResourceStorageType_S3 { if attachment.StorageType == storepb.AttachmentStorageType_EXTERNAL || attachment.StorageType == storepb.AttachmentStorageType_S3 {
enclosure.Url = resource.Reference enclosure.Url = attachment.Reference
} else { } else {
enclosure.Url = fmt.Sprintf("%s/file/attachments/%s/%s", baseURL, resource.UID, resource.Filename) enclosure.Url = fmt.Sprintf("%s/file/attachments/%s/%s", baseURL, attachment.UID, attachment.Filename)
} }
enclosure.Length = strconv.Itoa(int(resource.Size)) enclosure.Length = strconv.Itoa(int(attachment.Size))
enclosure.Type = resource.Type enclosure.Type = attachment.Type
feed.Items[i].Enclosure = &enclosure feed.Items[i].Enclosure = &enclosure
} }
} }
......
...@@ -49,33 +49,33 @@ func (r *Runner) CheckAndPresign(ctx context.Context) { ...@@ -49,33 +49,33 @@ func (r *Runner) CheckAndPresign(ctx context.Context) {
return return
} }
s3StorageType := storepb.ResourceStorageType_S3 s3StorageType := storepb.AttachmentStorageType_S3
// Limit resources to a reasonable batch size // Limit attachments to a reasonable batch size
const batchSize = 100 const batchSize = 100
offset := 0 offset := 0
for { for {
limit := batchSize limit := batchSize
resources, err := r.Store.ListResources(ctx, &store.FindResource{ attachments, err := r.Store.ListAttachments(ctx, &store.FindAttachment{
GetBlob: false, GetBlob: false,
StorageType: &s3StorageType, StorageType: &s3StorageType,
Limit: &limit, Limit: &limit,
Offset: &offset, Offset: &offset,
}) })
if err != nil { if err != nil {
slog.Error("Failed to list resources for presigning", "error", err) slog.Error("Failed to list attachments for presigning", "error", err)
return return
} }
// Break if no more resources // Break if no more attachments
if len(resources) == 0 { if len(attachments) == 0 {
break break
} }
// Process batch of resources // Process batch of attachments
presignCount := 0 presignCount := 0
for _, resource := range resources { for _, attachment := range attachments {
s3ObjectPayload := resource.Payload.GetS3Object() s3ObjectPayload := attachment.Payload.GetS3Object()
if s3ObjectPayload == nil { if s3ObjectPayload == nil {
continue continue
} }
...@@ -105,30 +105,30 @@ func (r *Runner) CheckAndPresign(ctx context.Context) { ...@@ -105,30 +105,30 @@ func (r *Runner) CheckAndPresign(ctx context.Context) {
presignURL, err := s3Client.PresignGetObject(ctx, s3ObjectPayload.Key) presignURL, err := s3Client.PresignGetObject(ctx, s3ObjectPayload.Key)
if err != nil { if err != nil {
slog.Error("Failed to presign URL", "error", err, "resourceID", resource.ID) slog.Error("Failed to presign URL", "error", err, "attachmentID", attachment.ID)
continue continue
} }
s3ObjectPayload.S3Config = s3Config s3ObjectPayload.S3Config = s3Config
s3ObjectPayload.LastPresignedTime = timestamppb.New(time.Now()) s3ObjectPayload.LastPresignedTime = timestamppb.New(time.Now())
if err := r.Store.UpdateResource(ctx, &store.UpdateResource{ if err := r.Store.UpdateAttachment(ctx, &store.UpdateAttachment{
ID: resource.ID, ID: attachment.ID,
Reference: &presignURL, Reference: &presignURL,
Payload: &storepb.ResourcePayload{ Payload: &storepb.AttachmentPayload{
Payload: &storepb.ResourcePayload_S3Object_{ Payload: &storepb.AttachmentPayload_S3Object_{
S3Object: s3ObjectPayload, S3Object: s3ObjectPayload,
}, },
}, },
}); err != nil { }); err != nil {
slog.Error("Failed to update resource", "error", err, "resourceID", resource.ID) slog.Error("Failed to update attachment", "error", err, "attachmentID", attachment.ID)
continue continue
} }
presignCount++ presignCount++
} }
slog.Info("Presigned batch of S3 resources", "batchSize", len(resources), "presigned", presignCount) slog.Info("Presigned batch of S3 attachments", "batchSize", len(attachments), "presigned", presignCount)
// Move to next batch // Move to next batch
offset += len(resources) offset += len(attachments)
} }
} }
...@@ -13,10 +13,10 @@ import ( ...@@ -13,10 +13,10 @@ import (
storepb "github.com/usememos/memos/proto/gen/store" storepb "github.com/usememos/memos/proto/gen/store"
) )
type Resource struct { type Attachment struct {
// ID is the system generated unique identifier for the resource. // ID is the system generated unique identifier for the attachment.
ID int32 ID int32
// UID is the user defined unique identifier for the resource. // UID is the user defined unique identifier for the attachment.
UID string UID string
// Standard fields // Standard fields
...@@ -29,15 +29,15 @@ type Resource struct { ...@@ -29,15 +29,15 @@ type Resource struct {
Blob []byte Blob []byte
Type string Type string
Size int64 Size int64
StorageType storepb.ResourceStorageType StorageType storepb.AttachmentStorageType
Reference string Reference string
Payload *storepb.ResourcePayload Payload *storepb.AttachmentPayload
// The related memo ID. // The related memo ID.
MemoID *int32 MemoID *int32
} }
type FindResource struct { type FindAttachment struct {
GetBlob bool GetBlob bool
ID *int32 ID *int32
UID *string UID *string
...@@ -46,35 +46,35 @@ type FindResource struct { ...@@ -46,35 +46,35 @@ type FindResource struct {
FilenameSearch *string FilenameSearch *string
MemoID *int32 MemoID *int32
HasRelatedMemo bool HasRelatedMemo bool
StorageType *storepb.ResourceStorageType StorageType *storepb.AttachmentStorageType
Limit *int Limit *int
Offset *int Offset *int
} }
type UpdateResource struct { type UpdateAttachment struct {
ID int32 ID int32
UID *string UID *string
UpdatedTs *int64 UpdatedTs *int64
Filename *string Filename *string
MemoID *int32 MemoID *int32
Reference *string Reference *string
Payload *storepb.ResourcePayload Payload *storepb.AttachmentPayload
} }
type DeleteResource struct { type DeleteAttachment struct {
ID int32 ID int32
MemoID *int32 MemoID *int32
} }
func (s *Store) CreateResource(ctx context.Context, create *Resource) (*Resource, error) { func (s *Store) CreateAttachment(ctx context.Context, create *Attachment) (*Attachment, error) {
if !base.UIDMatcher.MatchString(create.UID) { if !base.UIDMatcher.MatchString(create.UID) {
return nil, errors.New("invalid uid") return nil, errors.New("invalid uid")
} }
return s.driver.CreateResource(ctx, create) return s.driver.CreateAttachment(ctx, create)
} }
func (s *Store) ListResources(ctx context.Context, find *FindResource) ([]*Resource, error) { func (s *Store) ListAttachments(ctx context.Context, find *FindAttachment) ([]*Attachment, error) {
// Set default limits to prevent loading too many resources at once // Set default limits to prevent loading too many attachments at once
if find.Limit == nil && find.GetBlob { if find.Limit == nil && find.GetBlob {
// When fetching blobs, we should be especially careful with limits // When fetching blobs, we should be especially careful with limits
defaultLimit := 10 defaultLimit := 10
...@@ -85,41 +85,41 @@ func (s *Store) ListResources(ctx context.Context, find *FindResource) ([]*Resou ...@@ -85,41 +85,41 @@ func (s *Store) ListResources(ctx context.Context, find *FindResource) ([]*Resou
find.Limit = &defaultLimit find.Limit = &defaultLimit
} }
return s.driver.ListResources(ctx, find) return s.driver.ListAttachments(ctx, find)
} }
func (s *Store) GetResource(ctx context.Context, find *FindResource) (*Resource, error) { func (s *Store) GetAttachment(ctx context.Context, find *FindAttachment) (*Attachment, error) {
resources, err := s.ListResources(ctx, find) attachments, err := s.ListAttachments(ctx, find)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(resources) == 0 { if len(attachments) == 0 {
return nil, nil return nil, nil
} }
return resources[0], nil return attachments[0], nil
} }
func (s *Store) UpdateResource(ctx context.Context, update *UpdateResource) error { func (s *Store) UpdateAttachment(ctx context.Context, update *UpdateAttachment) error {
if update.UID != nil && !base.UIDMatcher.MatchString(*update.UID) { if update.UID != nil && !base.UIDMatcher.MatchString(*update.UID) {
return errors.New("invalid uid") return errors.New("invalid uid")
} }
return s.driver.UpdateResource(ctx, update) return s.driver.UpdateAttachment(ctx, update)
} }
func (s *Store) DeleteResource(ctx context.Context, delete *DeleteResource) error { func (s *Store) DeleteAttachment(ctx context.Context, delete *DeleteAttachment) error {
resource, err := s.GetResource(ctx, &FindResource{ID: &delete.ID}) attachment, err := s.GetAttachment(ctx, &FindAttachment{ID: &delete.ID})
if err != nil { if err != nil {
return errors.Wrap(err, "failed to get resource") return errors.Wrap(err, "failed to get attachment")
} }
if resource == nil { if attachment == nil {
return errors.New("resource not found") return errors.New("attachment not found")
} }
if resource.StorageType == storepb.ResourceStorageType_LOCAL { if attachment.StorageType == storepb.AttachmentStorageType_LOCAL {
if err := func() error { if err := func() error {
p := filepath.FromSlash(resource.Reference) p := filepath.FromSlash(attachment.Reference)
if !filepath.IsAbs(p) { if !filepath.IsAbs(p) {
p = filepath.Join(s.profile.Data, p) p = filepath.Join(s.profile.Data, p)
} }
...@@ -131,9 +131,9 @@ func (s *Store) DeleteResource(ctx context.Context, delete *DeleteResource) erro ...@@ -131,9 +131,9 @@ func (s *Store) DeleteResource(ctx context.Context, delete *DeleteResource) erro
}(); err != nil { }(); err != nil {
return errors.Wrap(err, "failed to delete local file") return errors.Wrap(err, "failed to delete local file")
} }
} else if resource.StorageType == storepb.ResourceStorageType_S3 { } else if attachment.StorageType == storepb.AttachmentStorageType_S3 {
if err := func() error { if err := func() error {
s3ObjectPayload := resource.Payload.GetS3Object() s3ObjectPayload := attachment.Payload.GetS3Object()
if s3ObjectPayload == nil { if s3ObjectPayload == nil {
return errors.Errorf("No s3 object found") return errors.Errorf("No s3 object found")
} }
...@@ -162,5 +162,5 @@ func (s *Store) DeleteResource(ctx context.Context, delete *DeleteResource) erro ...@@ -162,5 +162,5 @@ func (s *Store) DeleteResource(ctx context.Context, delete *DeleteResource) erro
} }
} }
return s.driver.DeleteResource(ctx, delete) return s.driver.DeleteAttachment(ctx, delete)
} }
...@@ -13,18 +13,18 @@ import ( ...@@ -13,18 +13,18 @@ import (
"github.com/usememos/memos/store" "github.com/usememos/memos/store"
) )
func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store.Resource, error) { func (d *DB) CreateAttachment(ctx context.Context, create *store.Attachment) (*store.Attachment, error) {
fields := []string{"`uid`", "`filename`", "`blob`", "`type`", "`size`", "`creator_id`", "`memo_id`", "`storage_type`", "`reference`", "`payload`"} fields := []string{"`uid`", "`filename`", "`blob`", "`type`", "`size`", "`creator_id`", "`memo_id`", "`storage_type`", "`reference`", "`payload`"}
placeholder := []string{"?", "?", "?", "?", "?", "?", "?", "?", "?", "?"} placeholder := []string{"?", "?", "?", "?", "?", "?", "?", "?", "?", "?"}
storageType := "" storageType := ""
if create.StorageType != storepb.ResourceStorageType_RESOURCE_STORAGE_TYPE_UNSPECIFIED { if create.StorageType != storepb.AttachmentStorageType_ATTACHMENT_STORAGE_TYPE_UNSPECIFIED {
storageType = create.StorageType.String() storageType = create.StorageType.String()
} }
payloadString := "{}" payloadString := "{}"
if create.Payload != nil { if create.Payload != nil {
bytes, err := protojson.Marshal(create.Payload) bytes, err := protojson.Marshal(create.Payload)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to marshal resource payload") return nil, errors.Wrap(err, "failed to marshal attachment payload")
} }
payloadString = string(bytes) payloadString = string(bytes)
} }
...@@ -42,10 +42,10 @@ func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store ...@@ -42,10 +42,10 @@ func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store
} }
id32 := int32(id) id32 := int32(id)
return d.GetResource(ctx, &store.FindResource{ID: &id32}) return d.GetAttachment(ctx, &store.FindAttachment{ID: &id32})
} }
func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*store.Resource, error) { func (d *DB) ListAttachments(ctx context.Context, find *store.FindAttachment) ([]*store.Attachment, error) {
where, args := []string{"1 = 1"}, []any{} where, args := []string{"1 = 1"}, []any{}
if v := find.ID; v != nil { if v := find.ID; v != nil {
...@@ -92,43 +92,43 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st ...@@ -92,43 +92,43 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
} }
defer rows.Close() defer rows.Close()
list := make([]*store.Resource, 0) list := make([]*store.Attachment, 0)
for rows.Next() { for rows.Next() {
resource := store.Resource{} attachment := store.Attachment{}
var memoID sql.NullInt32 var memoID sql.NullInt32
var storageType string var storageType string
var payloadBytes []byte var payloadBytes []byte
dests := []any{ dests := []any{
&resource.ID, &attachment.ID,
&resource.UID, &attachment.UID,
&resource.Filename, &attachment.Filename,
&resource.Type, &attachment.Type,
&resource.Size, &attachment.Size,
&resource.CreatorID, &attachment.CreatorID,
&resource.CreatedTs, &attachment.CreatedTs,
&resource.UpdatedTs, &attachment.UpdatedTs,
&memoID, &memoID,
&storageType, &storageType,
&resource.Reference, &attachment.Reference,
&payloadBytes, &payloadBytes,
} }
if find.GetBlob { if find.GetBlob {
dests = append(dests, &resource.Blob) dests = append(dests, &attachment.Blob)
} }
if err := rows.Scan(dests...); err != nil { if err := rows.Scan(dests...); err != nil {
return nil, err return nil, err
} }
if memoID.Valid { if memoID.Valid {
resource.MemoID = &memoID.Int32 attachment.MemoID = &memoID.Int32
} }
resource.StorageType = storepb.ResourceStorageType(storepb.ResourceStorageType_value[storageType]) attachment.StorageType = storepb.AttachmentStorageType(storepb.AttachmentStorageType_value[storageType])
payload := &storepb.ResourcePayload{} payload := &storepb.AttachmentPayload{}
if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil { if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {
return nil, err return nil, err
} }
resource.Payload = payload attachment.Payload = payload
list = append(list, &resource) list = append(list, &attachment)
} }
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
...@@ -138,8 +138,8 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st ...@@ -138,8 +138,8 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
return list, nil return list, nil
} }
func (d *DB) GetResource(ctx context.Context, find *store.FindResource) (*store.Resource, error) { func (d *DB) GetAttachment(ctx context.Context, find *store.FindAttachment) (*store.Attachment, error) {
list, err := d.ListResources(ctx, find) list, err := d.ListAttachments(ctx, find)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -150,7 +150,7 @@ func (d *DB) GetResource(ctx context.Context, find *store.FindResource) (*store. ...@@ -150,7 +150,7 @@ func (d *DB) GetResource(ctx context.Context, find *store.FindResource) (*store.
return list[0], nil return list[0], nil
} }
func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) error { func (d *DB) UpdateAttachment(ctx context.Context, update *store.UpdateAttachment) error {
set, args := []string{}, []any{} set, args := []string{}, []any{}
if v := update.UID; v != nil { if v := update.UID; v != nil {
...@@ -171,7 +171,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e ...@@ -171,7 +171,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
if v := update.Payload; v != nil { if v := update.Payload; v != nil {
bytes, err := protojson.Marshal(v) bytes, err := protojson.Marshal(v)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to marshal resource payload") return errors.Wrap(err, "failed to marshal attachment payload")
} }
set, args = append(set, "`payload` = ?"), append(args, string(bytes)) set, args = append(set, "`payload` = ?"), append(args, string(bytes))
} }
...@@ -188,7 +188,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e ...@@ -188,7 +188,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
return nil return nil
} }
func (d *DB) DeleteResource(ctx context.Context, delete *store.DeleteResource) error { func (d *DB) DeleteAttachment(ctx context.Context, delete *store.DeleteAttachment) error {
stmt := "DELETE FROM `resource` WHERE `id` = ?" stmt := "DELETE FROM `resource` WHERE `id` = ?"
result, err := d.db.ExecContext(ctx, stmt, delete.ID) result, err := d.db.ExecContext(ctx, stmt, delete.ID)
if err != nil { if err != nil {
......
...@@ -13,17 +13,17 @@ import ( ...@@ -13,17 +13,17 @@ import (
"github.com/usememos/memos/store" "github.com/usememos/memos/store"
) )
func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store.Resource, error) { func (d *DB) CreateAttachment(ctx context.Context, create *store.Attachment) (*store.Attachment, error) {
fields := []string{"uid", "filename", "blob", "type", "size", "creator_id", "memo_id", "storage_type", "reference", "payload"} fields := []string{"uid", "filename", "blob", "type", "size", "creator_id", "memo_id", "storage_type", "reference", "payload"}
storageType := "" storageType := ""
if create.StorageType != storepb.ResourceStorageType_RESOURCE_STORAGE_TYPE_UNSPECIFIED { if create.StorageType != storepb.AttachmentStorageType_ATTACHMENT_STORAGE_TYPE_UNSPECIFIED {
storageType = create.StorageType.String() storageType = create.StorageType.String()
} }
payloadString := "{}" payloadString := "{}"
if create.Payload != nil { if create.Payload != nil {
bytes, err := protojson.Marshal(create.Payload) bytes, err := protojson.Marshal(create.Payload)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to marshal resource payload") return nil, errors.Wrap(err, "failed to marshal attachment payload")
} }
payloadString = string(bytes) payloadString = string(bytes)
} }
...@@ -36,7 +36,7 @@ func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store ...@@ -36,7 +36,7 @@ func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store
return create, nil return create, nil
} }
func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*store.Resource, error) { func (d *DB) ListAttachments(ctx context.Context, find *store.FindAttachment) ([]*store.Attachment, error) {
where, args := []string{"1 = 1"}, []any{} where, args := []string{"1 = 1"}, []any{}
if v := find.ID; v != nil { if v := find.ID; v != nil {
...@@ -89,43 +89,43 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st ...@@ -89,43 +89,43 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
} }
defer rows.Close() defer rows.Close()
list := make([]*store.Resource, 0) list := make([]*store.Attachment, 0)
for rows.Next() { for rows.Next() {
resource := store.Resource{} attachment := store.Attachment{}
var memoID sql.NullInt32 var memoID sql.NullInt32
var storageType string var storageType string
var payloadBytes []byte var payloadBytes []byte
dests := []any{ dests := []any{
&resource.ID, &attachment.ID,
&resource.UID, &attachment.UID,
&resource.Filename, &attachment.Filename,
&resource.Type, &attachment.Type,
&resource.Size, &attachment.Size,
&resource.CreatorID, &attachment.CreatorID,
&resource.CreatedTs, &attachment.CreatedTs,
&resource.UpdatedTs, &attachment.UpdatedTs,
&memoID, &memoID,
&storageType, &storageType,
&resource.Reference, &attachment.Reference,
&payloadBytes, &payloadBytes,
} }
if find.GetBlob { if find.GetBlob {
dests = append(dests, &resource.Blob) dests = append(dests, &attachment.Blob)
} }
if err := rows.Scan(dests...); err != nil { if err := rows.Scan(dests...); err != nil {
return nil, err return nil, err
} }
if memoID.Valid { if memoID.Valid {
resource.MemoID = &memoID.Int32 attachment.MemoID = &memoID.Int32
} }
resource.StorageType = storepb.ResourceStorageType(storepb.ResourceStorageType_value[storageType]) attachment.StorageType = storepb.AttachmentStorageType(storepb.AttachmentStorageType_value[storageType])
payload := &storepb.ResourcePayload{} payload := &storepb.AttachmentPayload{}
if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil { if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {
return nil, err return nil, err
} }
resource.Payload = payload attachment.Payload = payload
list = append(list, &resource) list = append(list, &attachment)
} }
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
...@@ -135,7 +135,7 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st ...@@ -135,7 +135,7 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
return list, nil return list, nil
} }
func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) error { func (d *DB) UpdateAttachment(ctx context.Context, update *store.UpdateAttachment) error {
set, args := []string{}, []any{} set, args := []string{}, []any{}
if v := update.UID; v != nil { if v := update.UID; v != nil {
...@@ -156,7 +156,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e ...@@ -156,7 +156,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
if v := update.Payload; v != nil { if v := update.Payload; v != nil {
bytes, err := protojson.Marshal(v) bytes, err := protojson.Marshal(v)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to marshal resource payload") return errors.Wrap(err, "failed to marshal attachment payload")
} }
set, args = append(set, "payload = "+placeholder(len(args)+1)), append(args, string(bytes)) set, args = append(set, "payload = "+placeholder(len(args)+1)), append(args, string(bytes))
} }
...@@ -173,7 +173,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e ...@@ -173,7 +173,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
return nil return nil
} }
func (d *DB) DeleteResource(ctx context.Context, delete *store.DeleteResource) error { func (d *DB) DeleteAttachment(ctx context.Context, delete *store.DeleteAttachment) error {
stmt := `DELETE FROM resource WHERE id = $1` stmt := `DELETE FROM resource WHERE id = $1`
result, err := d.db.ExecContext(ctx, stmt, delete.ID) result, err := d.db.ExecContext(ctx, stmt, delete.ID)
if err != nil { if err != nil {
......
...@@ -13,18 +13,18 @@ import ( ...@@ -13,18 +13,18 @@ import (
"github.com/usememos/memos/store" "github.com/usememos/memos/store"
) )
func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store.Resource, error) { func (d *DB) CreateAttachment(ctx context.Context, create *store.Attachment) (*store.Attachment, error) {
fields := []string{"`uid`", "`filename`", "`blob`", "`type`", "`size`", "`creator_id`", "`memo_id`", "`storage_type`", "`reference`", "`payload`"} fields := []string{"`uid`", "`filename`", "`blob`", "`type`", "`size`", "`creator_id`", "`memo_id`", "`storage_type`", "`reference`", "`payload`"}
placeholder := []string{"?", "?", "?", "?", "?", "?", "?", "?", "?", "?"} placeholder := []string{"?", "?", "?", "?", "?", "?", "?", "?", "?", "?"}
storageType := "" storageType := ""
if create.StorageType != storepb.ResourceStorageType_RESOURCE_STORAGE_TYPE_UNSPECIFIED { if create.StorageType != storepb.AttachmentStorageType_ATTACHMENT_STORAGE_TYPE_UNSPECIFIED {
storageType = create.StorageType.String() storageType = create.StorageType.String()
} }
payloadString := "{}" payloadString := "{}"
if create.Payload != nil { if create.Payload != nil {
bytes, err := protojson.Marshal(create.Payload) bytes, err := protojson.Marshal(create.Payload)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to marshal resource payload") return nil, errors.Wrap(err, "failed to marshal attachment payload")
} }
payloadString = string(bytes) payloadString = string(bytes)
} }
...@@ -38,7 +38,7 @@ func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store ...@@ -38,7 +38,7 @@ func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store
return create, nil return create, nil
} }
func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*store.Resource, error) { func (d *DB) ListAttachments(ctx context.Context, find *store.FindAttachment) ([]*store.Attachment, error) {
where, args := []string{"1 = 1"}, []any{} where, args := []string{"1 = 1"}, []any{}
if v := find.ID; v != nil { if v := find.ID; v != nil {
...@@ -85,43 +85,43 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st ...@@ -85,43 +85,43 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
} }
defer rows.Close() defer rows.Close()
list := make([]*store.Resource, 0) list := make([]*store.Attachment, 0)
for rows.Next() { for rows.Next() {
resource := store.Resource{} attachment := store.Attachment{}
var memoID sql.NullInt32 var memoID sql.NullInt32
var storageType string var storageType string
var payloadBytes []byte var payloadBytes []byte
dests := []any{ dests := []any{
&resource.ID, &attachment.ID,
&resource.UID, &attachment.UID,
&resource.Filename, &attachment.Filename,
&resource.Type, &attachment.Type,
&resource.Size, &attachment.Size,
&resource.CreatorID, &attachment.CreatorID,
&resource.CreatedTs, &attachment.CreatedTs,
&resource.UpdatedTs, &attachment.UpdatedTs,
&memoID, &memoID,
&storageType, &storageType,
&resource.Reference, &attachment.Reference,
&payloadBytes, &payloadBytes,
} }
if find.GetBlob { if find.GetBlob {
dests = append(dests, &resource.Blob) dests = append(dests, &attachment.Blob)
} }
if err := rows.Scan(dests...); err != nil { if err := rows.Scan(dests...); err != nil {
return nil, err return nil, err
} }
if memoID.Valid { if memoID.Valid {
resource.MemoID = &memoID.Int32 attachment.MemoID = &memoID.Int32
} }
resource.StorageType = storepb.ResourceStorageType(storepb.ResourceStorageType_value[storageType]) attachment.StorageType = storepb.AttachmentStorageType(storepb.AttachmentStorageType_value[storageType])
payload := &storepb.ResourcePayload{} payload := &storepb.AttachmentPayload{}
if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil { if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {
return nil, err return nil, err
} }
resource.Payload = payload attachment.Payload = payload
list = append(list, &resource) list = append(list, &attachment)
} }
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
...@@ -131,7 +131,7 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st ...@@ -131,7 +131,7 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
return list, nil return list, nil
} }
func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) error { func (d *DB) UpdateAttachment(ctx context.Context, update *store.UpdateAttachment) error {
set, args := []string{}, []any{} set, args := []string{}, []any{}
if v := update.UID; v != nil { if v := update.UID; v != nil {
...@@ -152,7 +152,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e ...@@ -152,7 +152,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
if v := update.Payload; v != nil { if v := update.Payload; v != nil {
bytes, err := protojson.Marshal(v) bytes, err := protojson.Marshal(v)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to marshal resource payload") return errors.Wrap(err, "failed to marshal attachment payload")
} }
set, args = append(set, "`payload` = ?"), append(args, string(bytes)) set, args = append(set, "`payload` = ?"), append(args, string(bytes))
} }
...@@ -161,7 +161,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e ...@@ -161,7 +161,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
stmt := "UPDATE `resource` SET " + strings.Join(set, ", ") + " WHERE `id` = ?" stmt := "UPDATE `resource` SET " + strings.Join(set, ", ") + " WHERE `id` = ?"
result, err := d.db.ExecContext(ctx, stmt, args...) result, err := d.db.ExecContext(ctx, stmt, args...)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to update resource") return errors.Wrap(err, "failed to update attachment")
} }
if _, err := result.RowsAffected(); err != nil { if _, err := result.RowsAffected(); err != nil {
return err return err
...@@ -169,7 +169,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e ...@@ -169,7 +169,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
return nil return nil
} }
func (d *DB) DeleteResource(ctx context.Context, delete *store.DeleteResource) error { func (d *DB) DeleteAttachment(ctx context.Context, delete *store.DeleteAttachment) error {
stmt := "DELETE FROM `resource` WHERE `id` = ?" stmt := "DELETE FROM `resource` WHERE `id` = ?"
result, err := d.db.ExecContext(ctx, stmt, delete.ID) result, err := d.db.ExecContext(ctx, stmt, delete.ID)
if err != nil { if err != nil {
......
...@@ -25,11 +25,11 @@ type Driver interface { ...@@ -25,11 +25,11 @@ type Driver interface {
CreateActivity(ctx context.Context, create *Activity) (*Activity, error) CreateActivity(ctx context.Context, create *Activity) (*Activity, error)
ListActivities(ctx context.Context, find *FindActivity) ([]*Activity, error) ListActivities(ctx context.Context, find *FindActivity) ([]*Activity, error)
// Resource model related methods. // Attachment model related methods.
CreateResource(ctx context.Context, create *Resource) (*Resource, error) CreateAttachment(ctx context.Context, create *Attachment) (*Attachment, error)
ListResources(ctx context.Context, find *FindResource) ([]*Resource, error) ListAttachments(ctx context.Context, find *FindAttachment) ([]*Attachment, error)
UpdateResource(ctx context.Context, update *UpdateResource) error UpdateAttachment(ctx context.Context, update *UpdateAttachment) error
DeleteResource(ctx context.Context, delete *DeleteResource) error DeleteAttachment(ctx context.Context, delete *DeleteAttachment) error
// Memo model related methods. // Memo model related methods.
CreateMemo(ctx context.Context, create *Memo) (*Memo, error) CreateMemo(ctx context.Context, create *Memo) (*Memo, error)
......
...@@ -10,10 +10,10 @@ import ( ...@@ -10,10 +10,10 @@ import (
"github.com/usememos/memos/store" "github.com/usememos/memos/store"
) )
func TestResourceStore(t *testing.T) { func TestAttachmentStore(t *testing.T) {
ctx := context.Background() ctx := context.Background()
ts := NewTestingStore(ctx, t) ts := NewTestingStore(ctx, t)
_, err := ts.CreateResource(ctx, &store.Resource{ _, err := ts.CreateAttachment(ctx, &store.Attachment{
UID: shortuuid.New(), UID: shortuuid.New(),
CreatorID: 101, CreatorID: 101,
Filename: "test.epub", Filename: "test.epub",
...@@ -25,39 +25,39 @@ func TestResourceStore(t *testing.T) { ...@@ -25,39 +25,39 @@ func TestResourceStore(t *testing.T) {
correctFilename := "test.epub" correctFilename := "test.epub"
incorrectFilename := "test.png" incorrectFilename := "test.png"
resource, err := ts.GetResource(ctx, &store.FindResource{ attachment, err := ts.GetAttachment(ctx, &store.FindAttachment{
Filename: &correctFilename, Filename: &correctFilename,
}) })
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, correctFilename, resource.Filename) require.Equal(t, correctFilename, attachment.Filename)
require.Equal(t, int32(1), resource.ID) require.Equal(t, int32(1), attachment.ID)
notFoundResource, err := ts.GetResource(ctx, &store.FindResource{ notFoundAttachment, err := ts.GetAttachment(ctx, &store.FindAttachment{
Filename: &incorrectFilename, Filename: &incorrectFilename,
}) })
require.NoError(t, err) require.NoError(t, err)
require.Nil(t, notFoundResource) require.Nil(t, notFoundAttachment)
var correctCreatorID int32 = 101 var correctCreatorID int32 = 101
var incorrectCreatorID int32 = 102 var incorrectCreatorID int32 = 102
_, err = ts.GetResource(ctx, &store.FindResource{ _, err = ts.GetAttachment(ctx, &store.FindAttachment{
CreatorID: &correctCreatorID, CreatorID: &correctCreatorID,
}) })
require.NoError(t, err) require.NoError(t, err)
notFoundResource, err = ts.GetResource(ctx, &store.FindResource{ notFoundAttachment, err = ts.GetAttachment(ctx, &store.FindAttachment{
CreatorID: &incorrectCreatorID, CreatorID: &incorrectCreatorID,
}) })
require.NoError(t, err) require.NoError(t, err)
require.Nil(t, notFoundResource) require.Nil(t, notFoundAttachment)
err = ts.DeleteResource(ctx, &store.DeleteResource{ err = ts.DeleteAttachment(ctx, &store.DeleteAttachment{
ID: 1, ID: 1,
}) })
require.NoError(t, err) require.NoError(t, err)
err = ts.DeleteResource(ctx, &store.DeleteResource{ err = ts.DeleteAttachment(ctx, &store.DeleteAttachment{
ID: 2, ID: 2,
}) })
require.ErrorContains(t, err, "resource not found") require.ErrorContains(t, err, "attachment not found")
ts.Close() ts.Close()
} }
...@@ -17,15 +17,15 @@ import showPreviewImageDialog from "./PreviewImageDialog"; ...@@ -17,15 +17,15 @@ import showPreviewImageDialog from "./PreviewImageDialog";
import SquareDiv from "./kit/SquareDiv"; import SquareDiv from "./kit/SquareDiv";
interface Props { interface Props {
resource: Attachment; attachment: Attachment;
className?: string; className?: string;
strokeWidth?: number; strokeWidth?: number;
} }
const ResourceIcon = (props: Props) => { const AttachmentIcon = (props: Props) => {
const { resource } = props; const { attachment } = props;
const resourceType = getAttachmentType(resource); const resourceType = getAttachmentType(attachment);
const resourceUrl = getAttachmentUrl(resource); const resourceUrl = getAttachmentUrl(attachment);
const className = cn("w-full h-auto", props.className); const className = cn("w-full h-auto", props.className);
const strokeWidth = props.strokeWidth; const strokeWidth = props.strokeWidth;
...@@ -38,7 +38,7 @@ const ResourceIcon = (props: Props) => { ...@@ -38,7 +38,7 @@ const ResourceIcon = (props: Props) => {
<SquareDiv className={cn(className, "flex items-center justify-center overflow-clip")}> <SquareDiv className={cn(className, "flex items-center justify-center overflow-clip")}>
<img <img
className="min-w-full min-h-full object-cover" className="min-w-full min-h-full object-cover"
src={resource.externalLink ? resourceUrl : resourceUrl + "?thumbnail=true"} src={attachment.externalLink ? resourceUrl : resourceUrl + "?thumbnail=true"}
onClick={() => showPreviewImageDialog(resourceUrl)} onClick={() => showPreviewImageDialog(resourceUrl)}
decoding="async" decoding="async"
loading="lazy" loading="lazy"
...@@ -47,7 +47,7 @@ const ResourceIcon = (props: Props) => { ...@@ -47,7 +47,7 @@ const ResourceIcon = (props: Props) => {
); );
} }
const getResourceIcon = () => { const getAttachmentIcon = () => {
switch (resourceType) { switch (resourceType) {
case "video/*": case "video/*":
return <FileVideo2Icon strokeWidth={strokeWidth} className="w-full h-auto" />; return <FileVideo2Icon strokeWidth={strokeWidth} className="w-full h-auto" />;
...@@ -74,9 +74,9 @@ const ResourceIcon = (props: Props) => { ...@@ -74,9 +74,9 @@ const ResourceIcon = (props: Props) => {
return ( return (
<div onClick={previewResource} className={cn(className, "max-w-16 opacity-50")}> <div onClick={previewResource} className={cn(className, "max-w-16 opacity-50")}>
{getResourceIcon()} {getAttachmentIcon()}
</div> </div>
); );
}; };
export default React.memo(ResourceIcon); export default React.memo(AttachmentIcon);
import { Attachment } from "@/types/proto/api/v1/attachment_service";
import { getAttachmentUrl } from "@/utils/attachment";
import AttachmentIcon from "./AttachmentIcon";
interface Props {
attachment: Attachment;
className?: string;
}
const MemoAttachment: React.FC<Props> = (props: Props) => {
const { className, attachment } = props;
const attachmentUrl = getAttachmentUrl(attachment);
const handlePreviewBtnClick = () => {
window.open(attachmentUrl);
};
return (
<div className={`w-auto flex flex-row justify-start items-center text-gray-500 dark:text-gray-400 hover:opacity-80 ${className}`}>
{attachment.type.startsWith("audio") ? (
<audio src={attachmentUrl} controls></audio>
) : (
<>
<AttachmentIcon className="w-4! h-4! mr-1" attachment={attachment} />
<span className="text-sm max-w-[256px] truncate cursor-pointer" onClick={handlePreviewBtnClick}>
{attachment.filename}
</span>
</>
)}
</div>
);
};
export default MemoAttachment;
...@@ -2,7 +2,7 @@ import { memo } from "react"; ...@@ -2,7 +2,7 @@ import { memo } from "react";
import { Attachment } from "@/types/proto/api/v1/attachment_service"; import { Attachment } from "@/types/proto/api/v1/attachment_service";
import { cn } from "@/utils"; import { cn } from "@/utils";
import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment"; import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment";
import MemoResource from "./MemoResource"; import MemoAttachment from "./MemoAttachment";
import showPreviewImageDialog from "./PreviewImageDialog"; import showPreviewImageDialog from "./PreviewImageDialog";
const MemoAttachmentListView = ({ attachments = [] }: { attachments: Attachment[] }) => { const MemoAttachmentListView = ({ attachments = [] }: { attachments: Attachment[] }) => {
...@@ -78,7 +78,7 @@ const MemoAttachmentListView = ({ attachments = [] }: { attachments: Attachment[ ...@@ -78,7 +78,7 @@ const MemoAttachmentListView = ({ attachments = [] }: { attachments: Attachment[
return ( return (
<div className="w-full flex flex-row justify-start overflow-auto gap-2"> <div className="w-full flex flex-row justify-start overflow-auto gap-2">
{otherAttachments.map((attachment) => ( {otherAttachments.map((attachment) => (
<MemoResource key={attachment.name} resource={attachment} /> <MemoAttachment key={attachment.name} attachment={attachment} />
))} ))}
</div> </div>
); );
......
import { Button } from "@usememos/mui";
import { LoaderIcon, PaperclipIcon } from "lucide-react";
import { observer } from "mobx-react-lite";
import { useContext, useRef, useState } from "react";
import toast from "react-hot-toast";
import { attachmentStore } from "@/store/v2";
import { Attachment } from "@/types/proto/api/v1/attachment_service";
import { MemoEditorContext } from "../types";
interface Props {
isUploading?: boolean;
}
interface State {
uploadingFlag: boolean;
}
const UploadAttachmentButton = observer((props: Props) => {
const context = useContext(MemoEditorContext);
const [state, setState] = useState<State>({
uploadingFlag: false,
});
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileInputChange = async () => {
if (!fileInputRef.current || !fileInputRef.current.files || fileInputRef.current.files.length === 0) {
return;
}
if (state.uploadingFlag) {
return;
}
setState((state) => {
return {
...state,
uploadingFlag: true,
};
});
const createdAttachmentList: Attachment[] = [];
try {
if (!fileInputRef.current || !fileInputRef.current.files) {
return;
}
for (const file of fileInputRef.current.files) {
const { name: filename, size, type } = file;
const buffer = new Uint8Array(await file.arrayBuffer());
const attachment = await attachmentStore.createAttachment({
attachment: Attachment.fromPartial({
filename,
size,
type,
content: buffer,
}),
attachmentId: "",
});
createdAttachmentList.push(attachment);
}
} catch (error: any) {
console.error(error);
toast.error(error.details);
}
context.setAttachmentList([...context.attachmentList, ...createdAttachmentList]);
setState((state) => {
return {
...state,
uploadingFlag: false,
};
});
};
const isUploading = state.uploadingFlag || props.isUploading;
return (
<Button className="relative p-0" variant="plain" disabled={isUploading}>
{isUploading ? <LoaderIcon className="w-5 h-5 animate-spin" /> : <PaperclipIcon className="w-5 h-5" />}
<input
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
ref={fileInputRef}
disabled={isUploading}
onChange={handleFileInputChange}
type="file"
id="files"
multiple={true}
accept="*"
/>
</Button>
);
});
export default UploadAttachmentButton;
import { Button } from "@usememos/mui";
import { LoaderIcon, PaperclipIcon } from "lucide-react";
import { observer } from "mobx-react-lite";
import { useContext, useRef, useState } from "react";
import toast from "react-hot-toast";
import { attachmentStore } from "@/store/v2";
import { Attachment } from "@/types/proto/api/v1/attachment_service";
import { MemoEditorContext } from "../types";
interface Props {
isUploadingResource?: boolean;
}
interface State {
uploadingFlag: boolean;
}
const UploadResourceButton = observer((props: Props) => {
const context = useContext(MemoEditorContext);
const [state, setState] = useState<State>({
uploadingFlag: false,
});
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileInputChange = async () => {
if (!fileInputRef.current || !fileInputRef.current.files || fileInputRef.current.files.length === 0) {
return;
}
if (state.uploadingFlag) {
return;
}
setState((state) => {
return {
...state,
uploadingFlag: true,
};
});
const createdAttachmentList: Attachment[] = [];
try {
if (!fileInputRef.current || !fileInputRef.current.files) {
return;
}
for (const file of fileInputRef.current.files) {
const { name: filename, size, type } = file;
const buffer = new Uint8Array(await file.arrayBuffer());
const attachment = await attachmentStore.createAttachment({
attachment: Attachment.fromPartial({
filename,
size,
type,
content: buffer,
}),
attachmentId: "",
});
createdAttachmentList.push(attachment);
}
} catch (error: any) {
console.error(error);
toast.error(error.details);
}
context.setAttachmentList([...context.attachmentList, ...createdAttachmentList]);
setState((state) => {
return {
...state,
uploadingFlag: false,
};
});
};
const isUploading = state.uploadingFlag || props.isUploadingResource;
return (
<Button className="relative p-0" variant="plain" disabled={isUploading}>
{isUploading ? <LoaderIcon className="w-5 h-5 animate-spin" /> : <PaperclipIcon className="w-5 h-5" />}
<input
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
ref={fileInputRef}
disabled={isUploading}
onChange={handleFileInputChange}
type="file"
id="files"
multiple={true}
accept="*"
/>
</Button>
);
});
export default UploadResourceButton;
...@@ -2,7 +2,7 @@ import { DndContext, closestCenter, MouseSensor, TouchSensor, useSensor, useSens ...@@ -2,7 +2,7 @@ import { DndContext, closestCenter, MouseSensor, TouchSensor, useSensor, useSens
import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { XIcon } from "lucide-react"; import { XIcon } from "lucide-react";
import { Attachment } from "@/types/proto/api/v1/attachment_service"; import { Attachment } from "@/types/proto/api/v1/attachment_service";
import ResourceIcon from "../ResourceIcon"; import AttachmentIcon from "../AttachmentIcon";
import SortableItem from "./SortableItem"; import SortableItem from "./SortableItem";
interface Props { interface Props {
...@@ -41,7 +41,7 @@ const AttachmentListView = (props: Props) => { ...@@ -41,7 +41,7 @@ const AttachmentListView = (props: Props) => {
className="max-w-full w-auto flex flex-row justify-start items-center flex-nowrap gap-x-1 bg-zinc-100 dark:bg-zinc-900 px-2 py-1 rounded hover:shadow-sm text-gray-500 dark:text-gray-400" className="max-w-full w-auto flex flex-row justify-start items-center flex-nowrap gap-x-1 bg-zinc-100 dark:bg-zinc-900 px-2 py-1 rounded hover:shadow-sm text-gray-500 dark:text-gray-400"
> >
<SortableItem id={attachment.name} className="flex flex-row justify-start items-center gap-x-1"> <SortableItem id={attachment.name} className="flex flex-row justify-start items-center gap-x-1">
<ResourceIcon resource={attachment} className="w-4! h-4! opacity-100!" /> <AttachmentIcon attachment={attachment} className="w-4! h-4! opacity-100!" />
<span className="text-sm max-w-32 truncate">{attachment.filename}</span> <span className="text-sm max-w-32 truncate">{attachment.filename}</span>
</SortableItem> </SortableItem>
<button className="shrink-0" onClick={() => handleDeleteAttachment(attachment.name)}> <button className="shrink-0" onClick={() => handleDeleteAttachment(attachment.name)}>
......
...@@ -25,7 +25,7 @@ import AddMemoRelationPopover from "./ActionButton/AddMemoRelationPopover"; ...@@ -25,7 +25,7 @@ import AddMemoRelationPopover from "./ActionButton/AddMemoRelationPopover";
import LocationSelector from "./ActionButton/LocationSelector"; import LocationSelector from "./ActionButton/LocationSelector";
import MarkdownMenu from "./ActionButton/MarkdownMenu"; import MarkdownMenu from "./ActionButton/MarkdownMenu";
import TagSelector from "./ActionButton/TagSelector"; import TagSelector from "./ActionButton/TagSelector";
import UploadResourceButton from "./ActionButton/UploadResourceButton"; import UploadAttachmentButton from "./ActionButton/UploadAttachmentButton";
import VisibilitySelector from "./ActionButton/VisibilitySelector"; import VisibilitySelector from "./ActionButton/VisibilitySelector";
import AttachmentListView from "./AttachmentListView"; import AttachmentListView from "./AttachmentListView";
import Editor, { EditorRefActions } from "./Editor"; import Editor, { EditorRefActions } from "./Editor";
...@@ -51,7 +51,7 @@ interface State { ...@@ -51,7 +51,7 @@ interface State {
attachmentList: Attachment[]; attachmentList: Attachment[];
relationList: MemoRelation[]; relationList: MemoRelation[];
location: Location | undefined; location: Location | undefined;
isUploadingResource: boolean; isUploadingAttachment: boolean;
isRequesting: boolean; isRequesting: boolean;
isComposing: boolean; isComposing: boolean;
isDraggingFile: boolean; isDraggingFile: boolean;
...@@ -67,7 +67,7 @@ const MemoEditor = observer((props: Props) => { ...@@ -67,7 +67,7 @@ const MemoEditor = observer((props: Props) => {
attachmentList: [], attachmentList: [],
relationList: [], relationList: [],
location: undefined, location: undefined,
isUploadingResource: false, isUploadingAttachment: false,
isRequesting: false, isRequesting: false,
isComposing: false, isComposing: false,
isDraggingFile: false, isDraggingFile: false,
...@@ -203,7 +203,7 @@ const MemoEditor = observer((props: Props) => { ...@@ -203,7 +203,7 @@ const MemoEditor = observer((props: Props) => {
setState((state) => { setState((state) => {
return { return {
...state, ...state,
isUploadingResource: true, isUploadingAttachment: true,
}; };
}); });
...@@ -223,7 +223,7 @@ const MemoEditor = observer((props: Props) => { ...@@ -223,7 +223,7 @@ const MemoEditor = observer((props: Props) => {
setState((state) => { setState((state) => {
return { return {
...state, ...state,
isUploadingResource: false, isUploadingAttachment: false,
}; };
}); });
return attachment; return attachment;
...@@ -233,7 +233,7 @@ const MemoEditor = observer((props: Props) => { ...@@ -233,7 +233,7 @@ const MemoEditor = observer((props: Props) => {
setState((state) => { setState((state) => {
return { return {
...state, ...state,
isUploadingResource: false, isUploadingAttachment: false,
}; };
}); });
} }
...@@ -456,7 +456,7 @@ const MemoEditor = observer((props: Props) => { ...@@ -456,7 +456,7 @@ const MemoEditor = observer((props: Props) => {
[i18n.language], [i18n.language],
); );
const allowSave = (hasContent || state.attachmentList.length > 0) && !state.isUploadingResource && !state.isRequesting; const allowSave = (hasContent || state.attachmentList.length > 0) && !state.isUploadingAttachment && !state.isRequesting;
return ( return (
<MemoEditorContext.Provider <MemoEditorContext.Provider
...@@ -502,7 +502,7 @@ const MemoEditor = observer((props: Props) => { ...@@ -502,7 +502,7 @@ const MemoEditor = observer((props: Props) => {
<div className="flex flex-row justify-start items-center opacity-80 dark:opacity-60 space-x-2"> <div className="flex flex-row justify-start items-center opacity-80 dark:opacity-60 space-x-2">
<TagSelector editorRef={editorRef} /> <TagSelector editorRef={editorRef} />
<MarkdownMenu editorRef={editorRef} /> <MarkdownMenu editorRef={editorRef} />
<UploadResourceButton isUploadingResource={state.isUploadingResource} /> <UploadAttachmentButton isUploading={state.isUploadingAttachment} />
<AddMemoRelationPopover editorRef={editorRef} /> <AddMemoRelationPopover editorRef={editorRef} />
<LocationSelector <LocationSelector
location={state.location} location={state.location}
......
import { Attachment } from "@/types/proto/api/v1/attachment_service";
import { getAttachmentUrl } from "@/utils/attachment";
import ResourceIcon from "./ResourceIcon";
interface Props {
resource: Attachment;
className?: string;
}
const MemoResource: React.FC<Props> = (props: Props) => {
const { className, resource } = props;
const resourceUrl = getAttachmentUrl(resource);
const handlePreviewBtnClick = () => {
window.open(resourceUrl);
};
return (
<div className={`w-auto flex flex-row justify-start items-center text-gray-500 dark:text-gray-400 hover:opacity-80 ${className}`}>
{resource.type.startsWith("audio") ? (
<audio src={resourceUrl} controls></audio>
) : (
<>
<ResourceIcon className="w-4! h-4! mr-1" resource={resource} />
<span className="text-sm max-w-[256px] truncate cursor-pointer" onClick={handlePreviewBtnClick}>
{resource.filename}
</span>
</>
)}
</div>
);
};
export default MemoResource;
...@@ -5,9 +5,9 @@ import { includes } from "lodash-es"; ...@@ -5,9 +5,9 @@ import { includes } from "lodash-es";
import { PaperclipIcon, SearchIcon, TrashIcon } from "lucide-react"; import { PaperclipIcon, SearchIcon, TrashIcon } from "lucide-react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import AttachmentIcon from "@/components/AttachmentIcon";
import Empty from "@/components/Empty"; import Empty from "@/components/Empty";
import MobileHeader from "@/components/MobileHeader"; import MobileHeader from "@/components/MobileHeader";
import ResourceIcon from "@/components/ResourceIcon";
import { attachmentServiceClient } from "@/grpcweb"; import { attachmentServiceClient } from "@/grpcweb";
import useLoading from "@/hooks/useLoading"; import useLoading from "@/hooks/useLoading";
import useResponsiveWidth from "@/hooks/useResponsiveWidth"; import useResponsiveWidth from "@/hooks/useResponsiveWidth";
...@@ -112,7 +112,7 @@ const Attachments = observer(() => { ...@@ -112,7 +112,7 @@ const Attachments = observer(() => {
return ( return (
<div key={attachment.name} className="w-24 sm:w-32 h-auto flex flex-col justify-start items-start"> <div key={attachment.name} className="w-24 sm:w-32 h-auto flex flex-col justify-start items-start">
<div className="w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-zinc-200 dark:border-zinc-900 overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80"> <div className="w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-zinc-200 dark:border-zinc-900 overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80">
<ResourceIcon resource={attachment} strokeWidth={0.5} /> <AttachmentIcon attachment={attachment} strokeWidth={0.5} />
</div> </div>
<div className="w-full max-w-full flex flex-row justify-between items-center mt-1 px-1"> <div className="w-full max-w-full flex flex-row justify-between items-center mt-1 px-1">
<p className="text-xs shrink text-gray-400 truncate">{attachment.filename}</p> <p className="text-xs shrink text-gray-400 truncate">{attachment.filename}</p>
...@@ -144,7 +144,7 @@ const Attachments = observer(() => { ...@@ -144,7 +144,7 @@ const Attachments = observer(() => {
return ( return (
<div key={attachment.name} className="w-24 sm:w-32 h-auto flex flex-col justify-start items-start"> <div key={attachment.name} className="w-24 sm:w-32 h-auto flex flex-col justify-start items-start">
<div className="w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-zinc-200 dark:border-zinc-900 overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80"> <div className="w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-zinc-200 dark:border-zinc-900 overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80">
<ResourceIcon resource={attachment} strokeWidth={0.5} /> <AttachmentIcon attachment={attachment} strokeWidth={0.5} />
</div> </div>
<div className="w-full max-w-full flex flex-row justify-between items-center mt-1 px-1"> <div className="w-full max-w-full flex flex-row justify-between items-center mt-1 px-1">
<p className="text-xs shrink text-gray-400 truncate">{attachment.filename}</p> <p className="text-xs shrink text-gray-400 truncate">{attachment.filename}</p>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment