From a4920d464bac8f226c0ae494d35ac1504e9ec0b2 Mon Sep 17 00:00:00 2001 From: Steven Date: Wed, 18 Jun 2025 00:09:19 +0800 Subject: [PATCH] refactor: attachment service part2 --- plugin/httpgetter/http_getter.go | 3 - proto/gen/store/attachment.pb.go | 287 ++++++++++++++++++ proto/gen/store/resource.pb.go | 287 ------------------ .../{resource.proto => attachment.proto} | 12 +- server/router/api/v1/acl_config.go | 2 +- server/router/api/v1/attachment_service.go | 160 +++++----- .../router/api/v1/memo_attachment_service.go | 44 +-- server/router/api/v1/memo_service.go | 12 +- server/router/rss/rss.go | 16 +- server/runner/s3presign/runner.go | 34 +-- store/{resource.go => attachment.go} | 62 ++-- store/db/mysql/{resource.go => attachment.go} | 54 ++-- .../postgres/{resource.go => attachment.go} | 48 +-- .../db/sqlite/{resource.go => attachment.go} | 50 +-- store/driver.go | 10 +- .../{resource_test.go => attachment_test.go} | 26 +- .../{ResourceIcon.tsx => AttachmentIcon.tsx} | 18 +- web/src/components/MemoAttachment.tsx | 34 +++ web/src/components/MemoAttachmentListView.tsx | 4 +- .../ActionButton/UploadAttachmentButton.tsx | 92 ++++++ .../ActionButton/UploadResourceButton.tsx | 92 ------ .../MemoEditor/AttachmentListView.tsx | 4 +- web/src/components/MemoEditor/index.tsx | 16 +- web/src/components/MemoResource.tsx | 34 --- web/src/pages/Attachments.tsx | 6 +- 25 files changed, 702 insertions(+), 705 deletions(-) create mode 100644 proto/gen/store/attachment.pb.go delete mode 100644 proto/gen/store/resource.pb.go rename proto/store/{resource.proto => attachment.proto} (67%) rename store/{resource.go => attachment.go} (61%) rename store/db/mysql/{resource.go => attachment.go} (76%) rename store/db/postgres/{resource.go => attachment.go} (79%) rename store/db/sqlite/{resource.go => attachment.go} (77%) rename store/test/{resource_test.go => attachment_test.go} (55%) rename web/src/components/{ResourceIcon.tsx => AttachmentIcon.tsx} (85%) create mode 100644 web/src/components/MemoAttachment.tsx create mode 100644 web/src/components/MemoEditor/ActionButton/UploadAttachmentButton.tsx diff --git a/plugin/httpgetter/http_getter.go b/plugin/httpgetter/http_getter.go index c545baf68..581acb741 100644 --- a/plugin/httpgetter/http_getter.go +++ b/plugin/httpgetter/http_getter.go @@ -1,4 +1 @@ -// Package httpgetter is using to get resources from url. -// * Get metadata for website; -// * Get image blob to avoid CORS; package httpgetter diff --git a/proto/gen/store/attachment.pb.go b/proto/gen/store/attachment.pb.go new file mode 100644 index 000000000..1b70d40a2 --- /dev/null +++ b/proto/gen/store/attachment.pb.go @@ -0,0 +1,287 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: store/attachment.proto + +package store + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type AttachmentStorageType int32 + +const ( + AttachmentStorageType_ATTACHMENT_STORAGE_TYPE_UNSPECIFIED AttachmentStorageType = 0 + // Attachment is stored locally. AKA, local file system. + AttachmentStorageType_LOCAL AttachmentStorageType = 1 + // Attachment is stored in S3. + AttachmentStorageType_S3 AttachmentStorageType = 2 + // Attachment is stored in an external storage. The reference is a URL. + AttachmentStorageType_EXTERNAL AttachmentStorageType = 3 +) + +// Enum value maps for AttachmentStorageType. +var ( + AttachmentStorageType_name = map[int32]string{ + 0: "ATTACHMENT_STORAGE_TYPE_UNSPECIFIED", + 1: "LOCAL", + 2: "S3", + 3: "EXTERNAL", + } + AttachmentStorageType_value = map[string]int32{ + "ATTACHMENT_STORAGE_TYPE_UNSPECIFIED": 0, + "LOCAL": 1, + "S3": 2, + "EXTERNAL": 3, + } +) + +func (x AttachmentStorageType) Enum() *AttachmentStorageType { + p := new(AttachmentStorageType) + *p = x + return p +} + +func (x AttachmentStorageType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AttachmentStorageType) Descriptor() protoreflect.EnumDescriptor { + return file_store_attachment_proto_enumTypes[0].Descriptor() +} + +func (AttachmentStorageType) Type() protoreflect.EnumType { + return &file_store_attachment_proto_enumTypes[0] +} + +func (x AttachmentStorageType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AttachmentStorageType.Descriptor instead. +func (AttachmentStorageType) EnumDescriptor() ([]byte, []int) { + return file_store_attachment_proto_rawDescGZIP(), []int{0} +} + +type AttachmentPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *AttachmentPayload_S3Object_ + Payload isAttachmentPayload_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AttachmentPayload) Reset() { + *x = AttachmentPayload{} + mi := &file_store_attachment_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AttachmentPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttachmentPayload) ProtoMessage() {} + +func (x *AttachmentPayload) ProtoReflect() protoreflect.Message { + mi := &file_store_attachment_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AttachmentPayload.ProtoReflect.Descriptor instead. +func (*AttachmentPayload) Descriptor() ([]byte, []int) { + return file_store_attachment_proto_rawDescGZIP(), []int{0} +} + +func (x *AttachmentPayload) GetPayload() isAttachmentPayload_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *AttachmentPayload) GetS3Object() *AttachmentPayload_S3Object { + if x != nil { + if x, ok := x.Payload.(*AttachmentPayload_S3Object_); ok { + return x.S3Object + } + } + return nil +} + +type isAttachmentPayload_Payload interface { + isAttachmentPayload_Payload() +} + +type AttachmentPayload_S3Object_ struct { + S3Object *AttachmentPayload_S3Object `protobuf:"bytes,1,opt,name=s3_object,json=s3Object,proto3,oneof"` +} + +func (*AttachmentPayload_S3Object_) isAttachmentPayload_Payload() {} + +type AttachmentPayload_S3Object struct { + state protoimpl.MessageState `protogen:"open.v1"` + S3Config *StorageS3Config `protobuf:"bytes,1,opt,name=s3_config,json=s3Config,proto3" json:"s3_config,omitempty"` + // key is the S3 object key. + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + // last_presigned_time is the last time the object was presigned. + // This is used to determine if the presigned URL is still valid. + LastPresignedTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=last_presigned_time,json=lastPresignedTime,proto3" json:"last_presigned_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AttachmentPayload_S3Object) Reset() { + *x = AttachmentPayload_S3Object{} + mi := &file_store_attachment_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AttachmentPayload_S3Object) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttachmentPayload_S3Object) ProtoMessage() {} + +func (x *AttachmentPayload_S3Object) ProtoReflect() protoreflect.Message { + mi := &file_store_attachment_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AttachmentPayload_S3Object.ProtoReflect.Descriptor instead. +func (*AttachmentPayload_S3Object) Descriptor() ([]byte, []int) { + return file_store_attachment_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *AttachmentPayload_S3Object) GetS3Config() *StorageS3Config { + if x != nil { + return x.S3Config + } + return nil +} + +func (x *AttachmentPayload_S3Object) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *AttachmentPayload_S3Object) GetLastPresignedTime() *timestamppb.Timestamp { + if x != nil { + return x.LastPresignedTime + } + return nil +} + +var File_store_attachment_proto protoreflect.FileDescriptor + +const file_store_attachment_proto_rawDesc = "" + + "\n" + + "\x16store/attachment.proto\x12\vmemos.store\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1dstore/workspace_setting.proto\"\x8c\x02\n" + + "\x11AttachmentPayload\x12F\n" + + "\ts3_object\x18\x01 \x01(\v2'.memos.store.AttachmentPayload.S3ObjectH\x00R\bs3Object\x1a\xa3\x01\n" + + "\bS3Object\x129\n" + + "\ts3_config\x18\x01 \x01(\v2\x1c.memos.store.StorageS3ConfigR\bs3Config\x12\x10\n" + + "\x03key\x18\x02 \x01(\tR\x03key\x12J\n" + + "\x13last_presigned_time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x11lastPresignedTimeB\t\n" + + "\apayload*a\n" + + "\x15AttachmentStorageType\x12'\n" + + "#ATTACHMENT_STORAGE_TYPE_UNSPECIFIED\x10\x00\x12\t\n" + + "\x05LOCAL\x10\x01\x12\x06\n" + + "\x02S3\x10\x02\x12\f\n" + + "\bEXTERNAL\x10\x03B\x9a\x01\n" + + "\x0fcom.memos.storeB\x0fAttachmentProtoP\x01Z)github.com/usememos/memos/proto/gen/store\xa2\x02\x03MSX\xaa\x02\vMemos.Store\xca\x02\vMemos\\Store\xe2\x02\x17Memos\\Store\\GPBMetadata\xea\x02\fMemos::Storeb\x06proto3" + +var ( + file_store_attachment_proto_rawDescOnce sync.Once + file_store_attachment_proto_rawDescData []byte +) + +func file_store_attachment_proto_rawDescGZIP() []byte { + file_store_attachment_proto_rawDescOnce.Do(func() { + file_store_attachment_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_store_attachment_proto_rawDesc), len(file_store_attachment_proto_rawDesc))) + }) + return file_store_attachment_proto_rawDescData +} + +var file_store_attachment_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_store_attachment_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_store_attachment_proto_goTypes = []any{ + (AttachmentStorageType)(0), // 0: memos.store.AttachmentStorageType + (*AttachmentPayload)(nil), // 1: memos.store.AttachmentPayload + (*AttachmentPayload_S3Object)(nil), // 2: memos.store.AttachmentPayload.S3Object + (*StorageS3Config)(nil), // 3: memos.store.StorageS3Config + (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp +} +var file_store_attachment_proto_depIdxs = []int32{ + 2, // 0: memos.store.AttachmentPayload.s3_object:type_name -> memos.store.AttachmentPayload.S3Object + 3, // 1: memos.store.AttachmentPayload.S3Object.s3_config:type_name -> memos.store.StorageS3Config + 4, // 2: memos.store.AttachmentPayload.S3Object.last_presigned_time:type_name -> google.protobuf.Timestamp + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_store_attachment_proto_init() } +func file_store_attachment_proto_init() { + if File_store_attachment_proto != nil { + return + } + file_store_workspace_setting_proto_init() + file_store_attachment_proto_msgTypes[0].OneofWrappers = []any{ + (*AttachmentPayload_S3Object_)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_store_attachment_proto_rawDesc), len(file_store_attachment_proto_rawDesc)), + NumEnums: 1, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_store_attachment_proto_goTypes, + DependencyIndexes: file_store_attachment_proto_depIdxs, + EnumInfos: file_store_attachment_proto_enumTypes, + MessageInfos: file_store_attachment_proto_msgTypes, + }.Build() + File_store_attachment_proto = out.File + file_store_attachment_proto_goTypes = nil + file_store_attachment_proto_depIdxs = nil +} diff --git a/proto/gen/store/resource.pb.go b/proto/gen/store/resource.pb.go deleted file mode 100644 index fe5e60fe0..000000000 --- a/proto/gen/store/resource.pb.go +++ /dev/null @@ -1,287 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.36.6 -// protoc (unknown) -// source: store/resource.proto - -package store - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" - unsafe "unsafe" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type ResourceStorageType int32 - -const ( - ResourceStorageType_RESOURCE_STORAGE_TYPE_UNSPECIFIED ResourceStorageType = 0 - // Resource is stored locally. AKA, local file system. - ResourceStorageType_LOCAL ResourceStorageType = 1 - // Resource is stored in S3. - ResourceStorageType_S3 ResourceStorageType = 2 - // Resource is stored in an external storage. The reference is a URL. - ResourceStorageType_EXTERNAL ResourceStorageType = 3 -) - -// Enum value maps for ResourceStorageType. -var ( - ResourceStorageType_name = map[int32]string{ - 0: "RESOURCE_STORAGE_TYPE_UNSPECIFIED", - 1: "LOCAL", - 2: "S3", - 3: "EXTERNAL", - } - ResourceStorageType_value = map[string]int32{ - "RESOURCE_STORAGE_TYPE_UNSPECIFIED": 0, - "LOCAL": 1, - "S3": 2, - "EXTERNAL": 3, - } -) - -func (x ResourceStorageType) Enum() *ResourceStorageType { - p := new(ResourceStorageType) - *p = x - return p -} - -func (x ResourceStorageType) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (ResourceStorageType) Descriptor() protoreflect.EnumDescriptor { - return file_store_resource_proto_enumTypes[0].Descriptor() -} - -func (ResourceStorageType) Type() protoreflect.EnumType { - return &file_store_resource_proto_enumTypes[0] -} - -func (x ResourceStorageType) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use ResourceStorageType.Descriptor instead. -func (ResourceStorageType) EnumDescriptor() ([]byte, []int) { - return file_store_resource_proto_rawDescGZIP(), []int{0} -} - -type ResourcePayload struct { - state protoimpl.MessageState `protogen:"open.v1"` - // Types that are valid to be assigned to Payload: - // - // *ResourcePayload_S3Object_ - Payload isResourcePayload_Payload `protobuf_oneof:"payload"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ResourcePayload) Reset() { - *x = ResourcePayload{} - mi := &file_store_resource_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ResourcePayload) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ResourcePayload) ProtoMessage() {} - -func (x *ResourcePayload) ProtoReflect() protoreflect.Message { - mi := &file_store_resource_proto_msgTypes[0] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ResourcePayload.ProtoReflect.Descriptor instead. -func (*ResourcePayload) Descriptor() ([]byte, []int) { - return file_store_resource_proto_rawDescGZIP(), []int{0} -} - -func (x *ResourcePayload) GetPayload() isResourcePayload_Payload { - if x != nil { - return x.Payload - } - return nil -} - -func (x *ResourcePayload) GetS3Object() *ResourcePayload_S3Object { - if x != nil { - if x, ok := x.Payload.(*ResourcePayload_S3Object_); ok { - return x.S3Object - } - } - return nil -} - -type isResourcePayload_Payload interface { - isResourcePayload_Payload() -} - -type ResourcePayload_S3Object_ struct { - S3Object *ResourcePayload_S3Object `protobuf:"bytes,1,opt,name=s3_object,json=s3Object,proto3,oneof"` -} - -func (*ResourcePayload_S3Object_) isResourcePayload_Payload() {} - -type ResourcePayload_S3Object struct { - state protoimpl.MessageState `protogen:"open.v1"` - S3Config *StorageS3Config `protobuf:"bytes,1,opt,name=s3_config,json=s3Config,proto3" json:"s3_config,omitempty"` - // key is the S3 object key. - Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` - // last_presigned_time is the last time the object was presigned. - // This is used to determine if the presigned URL is still valid. - LastPresignedTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=last_presigned_time,json=lastPresignedTime,proto3" json:"last_presigned_time,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ResourcePayload_S3Object) Reset() { - *x = ResourcePayload_S3Object{} - mi := &file_store_resource_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ResourcePayload_S3Object) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ResourcePayload_S3Object) ProtoMessage() {} - -func (x *ResourcePayload_S3Object) ProtoReflect() protoreflect.Message { - mi := &file_store_resource_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ResourcePayload_S3Object.ProtoReflect.Descriptor instead. -func (*ResourcePayload_S3Object) Descriptor() ([]byte, []int) { - return file_store_resource_proto_rawDescGZIP(), []int{0, 0} -} - -func (x *ResourcePayload_S3Object) GetS3Config() *StorageS3Config { - if x != nil { - return x.S3Config - } - return nil -} - -func (x *ResourcePayload_S3Object) GetKey() string { - if x != nil { - return x.Key - } - return "" -} - -func (x *ResourcePayload_S3Object) GetLastPresignedTime() *timestamppb.Timestamp { - if x != nil { - return x.LastPresignedTime - } - return nil -} - -var File_store_resource_proto protoreflect.FileDescriptor - -const file_store_resource_proto_rawDesc = "" + - "\n" + - "\x14store/resource.proto\x12\vmemos.store\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1dstore/workspace_setting.proto\"\x88\x02\n" + - "\x0fResourcePayload\x12D\n" + - "\ts3_object\x18\x01 \x01(\v2%.memos.store.ResourcePayload.S3ObjectH\x00R\bs3Object\x1a\xa3\x01\n" + - "\bS3Object\x129\n" + - "\ts3_config\x18\x01 \x01(\v2\x1c.memos.store.StorageS3ConfigR\bs3Config\x12\x10\n" + - "\x03key\x18\x02 \x01(\tR\x03key\x12J\n" + - "\x13last_presigned_time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x11lastPresignedTimeB\t\n" + - "\apayload*]\n" + - "\x13ResourceStorageType\x12%\n" + - "!RESOURCE_STORAGE_TYPE_UNSPECIFIED\x10\x00\x12\t\n" + - "\x05LOCAL\x10\x01\x12\x06\n" + - "\x02S3\x10\x02\x12\f\n" + - "\bEXTERNAL\x10\x03B\x98\x01\n" + - "\x0fcom.memos.storeB\rResourceProtoP\x01Z)github.com/usememos/memos/proto/gen/store\xa2\x02\x03MSX\xaa\x02\vMemos.Store\xca\x02\vMemos\\Store\xe2\x02\x17Memos\\Store\\GPBMetadata\xea\x02\fMemos::Storeb\x06proto3" - -var ( - file_store_resource_proto_rawDescOnce sync.Once - file_store_resource_proto_rawDescData []byte -) - -func file_store_resource_proto_rawDescGZIP() []byte { - file_store_resource_proto_rawDescOnce.Do(func() { - file_store_resource_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_store_resource_proto_rawDesc), len(file_store_resource_proto_rawDesc))) - }) - return file_store_resource_proto_rawDescData -} - -var file_store_resource_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_store_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_store_resource_proto_goTypes = []any{ - (ResourceStorageType)(0), // 0: memos.store.ResourceStorageType - (*ResourcePayload)(nil), // 1: memos.store.ResourcePayload - (*ResourcePayload_S3Object)(nil), // 2: memos.store.ResourcePayload.S3Object - (*StorageS3Config)(nil), // 3: memos.store.StorageS3Config - (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp -} -var file_store_resource_proto_depIdxs = []int32{ - 2, // 0: memos.store.ResourcePayload.s3_object:type_name -> memos.store.ResourcePayload.S3Object - 3, // 1: memos.store.ResourcePayload.S3Object.s3_config:type_name -> memos.store.StorageS3Config - 4, // 2: memos.store.ResourcePayload.S3Object.last_presigned_time:type_name -> google.protobuf.Timestamp - 3, // [3:3] is the sub-list for method output_type - 3, // [3:3] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name -} - -func init() { file_store_resource_proto_init() } -func file_store_resource_proto_init() { - if File_store_resource_proto != nil { - return - } - file_store_workspace_setting_proto_init() - file_store_resource_proto_msgTypes[0].OneofWrappers = []any{ - (*ResourcePayload_S3Object_)(nil), - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_store_resource_proto_rawDesc), len(file_store_resource_proto_rawDesc)), - NumEnums: 1, - NumMessages: 2, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_store_resource_proto_goTypes, - DependencyIndexes: file_store_resource_proto_depIdxs, - EnumInfos: file_store_resource_proto_enumTypes, - MessageInfos: file_store_resource_proto_msgTypes, - }.Build() - File_store_resource_proto = out.File - file_store_resource_proto_goTypes = nil - file_store_resource_proto_depIdxs = nil -} diff --git a/proto/store/resource.proto b/proto/store/attachment.proto similarity index 67% rename from proto/store/resource.proto rename to proto/store/attachment.proto index 57c7096ce..4527a8af5 100644 --- a/proto/store/resource.proto +++ b/proto/store/attachment.proto @@ -7,17 +7,17 @@ import "store/workspace_setting.proto"; option go_package = "gen/store"; -enum ResourceStorageType { - RESOURCE_STORAGE_TYPE_UNSPECIFIED = 0; - // Resource is stored locally. AKA, local file system. +enum AttachmentStorageType { + ATTACHMENT_STORAGE_TYPE_UNSPECIFIED = 0; + // Attachment is stored locally. AKA, local file system. LOCAL = 1; - // Resource is stored in S3. + // Attachment is stored in S3. 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; } -message ResourcePayload { +message AttachmentPayload { oneof payload { S3Object s3_object = 1; } diff --git a/server/router/api/v1/acl_config.go b/server/router/api/v1/acl_config.go index 1c96a0613..631d0d3cf 100644 --- a/server/router/api/v1/acl_config.go +++ b/server/router/api/v1/acl_config.go @@ -18,7 +18,7 @@ var authenticationAllowlistMethods = map[string]bool{ "/memos.api.v1.MemoService/GetMemo": true, "/memos.api.v1.MemoService/ListMemos": 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. diff --git a/server/router/api/v1/attachment_service.go b/server/router/api/v1/attachment_service.go index d11b7e9ca..d4a51f72e 100644 --- a/server/router/api/v1/attachment_service.go +++ b/server/router/api/v1/attachment_service.go @@ -68,7 +68,7 @@ func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.Creat attachmentUID = shortuuid.New() } - create := &store.Resource{ + create := &store.Attachment{ UID: attachmentUID, CreatorID: user.ID, Filename: request.Attachment.Filename, @@ -90,8 +90,8 @@ func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.Creat create.Size = int64(size) create.Blob = request.Attachment.Content - if err := SaveResourceBlob(ctx, s.Profile, s.Store, create); err != nil { - return nil, status.Errorf(codes.Internal, "failed to save resource blob: %v", err) + if err := SaveAttachmentBlob(ctx, s.Profile, s.Store, create); err != nil { + return nil, status.Errorf(codes.Internal, "failed to save attachment blob: %v", err) } if request.Attachment.Memo != nil { @@ -108,12 +108,12 @@ func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.Creat } create.MemoID = &memo.ID } - resource, err := s.Store.CreateResource(ctx, create) + attachment, err := s.Store.CreateAttachment(ctx, create) if err != nil { - return nil, status.Errorf(codes.Internal, "failed to create resource: %v", err) + return nil, status.Errorf(codes.Internal, "failed to create attachment: %v", err) } - return s.convertAttachmentFromStore(ctx, resource), nil + return s.convertAttachmentFromStore(ctx, attachment), nil } func (s *APIV1Service) ListAttachments(ctx context.Context, request *v1pb.ListAttachmentsRequest) (*v1pb.ListAttachmentsResponse, error) { @@ -141,7 +141,7 @@ func (s *APIV1Service) ListAttachments(ctx context.Context, request *v1pb.ListAt } } - findResource := &store.FindResource{ + findAttachment := &store.FindAttachment{ CreatorID: &user.ID, Limit: &pageSize, Offset: &offset, @@ -154,40 +154,40 @@ func (s *APIV1Service) ListAttachments(ctx context.Context, request *v1pb.ListAt if strings.HasPrefix(request.Filter, "type=") { filterType := strings.TrimPrefix(request.Filter, "type=") // Create a temporary struct to hold type filter - // Since FindResource doesn't have Type field, we'll apply this post-query + // Since FindAttachment doesn't have Type field, we'll apply this post-query _ = filterType // We'll filter after getting results } } - resources, err := s.Store.ListResources(ctx, findResource) + attachments, err := s.Store.ListAttachments(ctx, findAttachment) 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) } // Apply type filter if specified if request.Filter != "" && strings.HasPrefix(request.Filter, "type=") { filterType := strings.TrimPrefix(request.Filter, "type=") - filteredResources := make([]*store.Resource, 0) - for _, resource := range resources { - if resource.Type == filterType { - filteredResources = append(filteredResources, resource) + filteredAttachments := make([]*store.Attachment, 0) + for _, attachment := range attachments { + if attachment.Type == filterType { + filteredAttachments = append(filteredAttachments, attachment) } } - resources = filteredResources + attachments = filteredAttachments } response := &v1pb.ListAttachmentsResponse{} - for _, resource := range resources { - response.Attachments = append(response.Attachments, s.convertAttachmentFromStore(ctx, resource)) + for _, attachment := range attachments { + response.Attachments = append(response.Attachments, s.convertAttachmentFromStore(ctx, attachment)) } - // For simplicity, set total size to the number of returned resources + // For simplicity, set total size to the number of returned attachments. // In a full implementation, you'd want a separate count query response.TotalSize = int32(len(response.Attachments)) // Set next page token if we got the full page size (indicating there might be more) - if len(resources) == pageSize { + if len(attachments) == pageSize { response.NextPageToken = fmt.Sprintf("%d", offset+pageSize) } @@ -199,14 +199,14 @@ func (s *APIV1Service) GetAttachment(ctx context.Context, request *v1pb.GetAttac if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid attachment id: %v", err) } - resource, err := s.Store.GetResource(ctx, &store.FindResource{UID: &attachmentUID}) + attachment, err := s.Store.GetAttachment(ctx, &store.FindAttachment{UID: &attachmentUID}) 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) } - if resource == nil { + if attachment == nil { return nil, status.Errorf(codes.NotFound, "attachment not found") } - return s.convertAttachmentFromStore(ctx, resource), nil + return s.convertAttachmentFromStore(ctx, attachment), nil } func (s *APIV1Service) GetAttachmentBinary(ctx context.Context, request *v1pb.GetAttachmentBinaryRequest) (*httpbody.HttpBody, error) { @@ -214,23 +214,23 @@ func (s *APIV1Service) GetAttachmentBinary(ctx context.Context, request *v1pb.Ge if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid attachment id: %v", err) } - resource, err := s.Store.GetResource(ctx, &store.FindResource{ + attachment, err := s.Store.GetAttachment(ctx, &store.FindAttachment{ GetBlob: true, UID: &attachmentUID, }) 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) } - if resource == nil { + if attachment == nil { return nil, status.Errorf(codes.NotFound, "attachment not found") } // Check the related memo visibility. - if resource.MemoID != nil { + if attachment.MemoID != nil { memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ - ID: resource.MemoID, + ID: attachment.MemoID, }) if err != nil { - return nil, status.Errorf(codes.Internal, "failed to find memo by ID: %v", resource.MemoID) + return nil, status.Errorf(codes.Internal, "failed to find memo by ID: %v", attachment.MemoID) } if memo != nil && memo.Visibility != store.Public { user, err := s.GetCurrentUser(ctx) @@ -240,32 +240,32 @@ func (s *APIV1Service) GetAttachmentBinary(ctx context.Context, request *v1pb.Ge if user == nil { return nil, status.Errorf(codes.Unauthenticated, "unauthorized access") } - if memo.Visibility == store.Private && user.ID != resource.CreatorID { + if memo.Visibility == store.Private && user.ID != attachment.CreatorID { return nil, status.Errorf(codes.Unauthenticated, "unauthorized access") } } } - if request.Thumbnail && util.HasPrefixes(resource.Type, SupportedThumbnailMimeTypes...) { - thumbnailBlob, err := s.getOrGenerateThumbnail(resource) + if request.Thumbnail && util.HasPrefixes(attachment.Type, SupportedThumbnailMimeTypes...) { + thumbnailBlob, err := s.getOrGenerateThumbnail(attachment) if err != nil { // thumbnail failures are logged as warnings and not cosidered critical failures as - // a resource image can be used in its place. - slog.Warn("failed to get resource thumbnail image", slog.Any("error", err)) + // a attachment image can be used in its place. + slog.Warn("failed to get attachment thumbnail image", slog.Any("error", err)) } else { return &httpbody.HttpBody{ - ContentType: resource.Type, + ContentType: attachment.Type, Data: thumbnailBlob, }, nil } } - blob, err := s.GetResourceBlob(resource) + blob, err := s.GetAttachmentBlob(attachment) if err != nil { - return nil, status.Errorf(codes.Internal, "failed to get resource blob: %v", err) + return nil, status.Errorf(codes.Internal, "failed to get attachment blob: %v", err) } - contentType := resource.Type + contentType := attachment.Type if strings.HasPrefix(contentType, "text/") { contentType += "; charset=utf-8" } @@ -290,14 +290,14 @@ func (s *APIV1Service) UpdateAttachment(ctx context.Context, request *v1pb.Updat if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 { return nil, status.Errorf(codes.InvalidArgument, "update mask is required") } - resource, err := s.Store.GetResource(ctx, &store.FindResource{UID: &attachmentUID}) + attachment, err := s.Store.GetAttachment(ctx, &store.FindAttachment{UID: &attachmentUID}) 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) } currentTs := time.Now().Unix() - update := &store.UpdateResource{ - ID: resource.ID, + update := &store.UpdateAttachment{ + ID: attachment.ID, UpdatedTs: ¤tTs, } for _, field := range request.UpdateMask.Paths { @@ -306,8 +306,8 @@ func (s *APIV1Service) UpdateAttachment(ctx context.Context, request *v1pb.Updat } } - if err := s.Store.UpdateResource(ctx, update); err != nil { - return nil, status.Errorf(codes.Internal, "failed to update resource: %v", err) + if err := s.Store.UpdateAttachment(ctx, update); err != nil { + return nil, status.Errorf(codes.Internal, "failed to update attachment: %v", err) } return s.GetAttachment(ctx, &v1pb.GetAttachmentRequest{ Name: request.Attachment.Name, @@ -323,39 +323,39 @@ func (s *APIV1Service) DeleteAttachment(ctx context.Context, request *v1pb.Delet if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) } - resource, err := s.Store.GetResource(ctx, &store.FindResource{ + attachment, err := s.Store.GetAttachment(ctx, &store.FindAttachment{ UID: &attachmentUID, CreatorID: &user.ID, }) if err != nil { - return nil, status.Errorf(codes.Internal, "failed to find resource: %v", err) + return nil, status.Errorf(codes.Internal, "failed to find attachment: %v", err) } - if resource == nil { + if attachment == nil { return nil, status.Errorf(codes.NotFound, "attachment not found") } - // Delete the resource from the database. - if err := s.Store.DeleteResource(ctx, &store.DeleteResource{ - ID: resource.ID, + // Delete the attachment from the database. + if err := s.Store.DeleteAttachment(ctx, &store.DeleteAttachment{ + ID: attachment.ID, }); err != nil { - return nil, status.Errorf(codes.Internal, "failed to delete resource: %v", err) + return nil, status.Errorf(codes.Internal, "failed to delete attachment: %v", err) } return &emptypb.Empty{}, nil } -func (s *APIV1Service) convertAttachmentFromStore(ctx context.Context, resource *store.Resource) *v1pb.Attachment { +func (s *APIV1Service) convertAttachmentFromStore(ctx context.Context, attachment *store.Attachment) *v1pb.Attachment { attachmentMessage := &v1pb.Attachment{ - Name: fmt.Sprintf("%s%s", AttachmentNamePrefix, resource.UID), - CreateTime: timestamppb.New(time.Unix(resource.CreatedTs, 0)), - Filename: resource.Filename, - Type: resource.Type, - Size: resource.Size, + Name: fmt.Sprintf("%s%s", AttachmentNamePrefix, attachment.UID), + CreateTime: timestamppb.New(time.Unix(attachment.CreatedTs, 0)), + Filename: attachment.Filename, + Type: attachment.Type, + Size: attachment.Size, } - if resource.StorageType == storepb.ResourceStorageType_EXTERNAL || resource.StorageType == storepb.ResourceStorageType_S3 { - attachmentMessage.ExternalLink = resource.Reference + if attachment.StorageType == storepb.AttachmentStorageType_EXTERNAL || attachment.StorageType == storepb.AttachmentStorageType_S3 { + attachmentMessage.ExternalLink = attachment.Reference } - if resource.MemoID != nil { + if attachment.MemoID != nil { memo, _ := s.Store.GetMemo(ctx, &store.FindMemo{ - ID: resource.MemoID, + ID: attachment.MemoID, }) if memo != nil { memoName := fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID) @@ -366,8 +366,8 @@ func (s *APIV1Service) convertAttachmentFromStore(ctx context.Context, resource return attachmentMessage } -// SaveResourceBlob save the blob of resource based on the storage config. -func SaveResourceBlob(ctx context.Context, profile *profile.Profile, stores *store.Store, create *store.Resource) error { +// SaveAttachmentBlob save the blob of attachment based on the storage config. +func SaveAttachmentBlob(ctx context.Context, profile *profile.Profile, stores *store.Store, create *store.Attachment) error { workspaceStorageSetting, err := stores.GetWorkspaceStorageSetting(ctx) if err != nil { return errors.Wrap(err, "Failed to find workspace storage setting") @@ -407,7 +407,7 @@ func SaveResourceBlob(ctx context.Context, profile *profile.Profile, stores *sto } create.Reference = internalPath create.Blob = nil - create.StorageType = storepb.ResourceStorageType_LOCAL + create.StorageType = storepb.AttachmentStorageType_LOCAL } else if workspaceStorageSetting.StorageType == storepb.WorkspaceStorageSetting_S3 { s3Config := workspaceStorageSetting.S3Config if s3Config == nil { @@ -434,10 +434,10 @@ func SaveResourceBlob(ctx context.Context, profile *profile.Profile, stores *sto create.Reference = presignURL create.Blob = nil - create.StorageType = storepb.ResourceStorageType_S3 - create.Payload = &storepb.ResourcePayload{ - Payload: &storepb.ResourcePayload_S3Object_{ - S3Object: &storepb.ResourcePayload_S3Object{ + create.StorageType = storepb.AttachmentStorageType_S3 + create.Payload = &storepb.AttachmentPayload{ + Payload: &storepb.AttachmentPayload_S3Object_{ + S3Object: &storepb.AttachmentPayload_S3Object{ S3Config: s3Config, Key: key, LastPresignedTime: timestamppb.New(time.Now()), @@ -449,15 +449,15 @@ func SaveResourceBlob(ctx context.Context, profile *profile.Profile, stores *sto return nil } -func (s *APIV1Service) GetResourceBlob(resource *store.Resource) ([]byte, error) { +func (s *APIV1Service) GetAttachmentBlob(attachment *store.Attachment) ([]byte, error) { // For local storage, read the file from the local disk. - if resource.StorageType == storepb.ResourceStorageType_LOCAL { - resourcePath := filepath.FromSlash(resource.Reference) - if !filepath.IsAbs(resourcePath) { - resourcePath = filepath.Join(s.Profile.Data, resourcePath) + if attachment.StorageType == storepb.AttachmentStorageType_LOCAL { + attachmentPath := filepath.FromSlash(attachment.Reference) + if !filepath.IsAbs(attachmentPath) { + attachmentPath = filepath.Join(s.Profile.Data, attachmentPath) } - file, err := os.Open(resourcePath) + file, err := os.Open(attachmentPath) if err != nil { if os.IsNotExist(err) { return nil, errors.Wrap(err, "file not found") @@ -472,7 +472,7 @@ func (s *APIV1Service) GetResourceBlob(resource *store.Resource) ([]byte, error) return blob, nil } // For database storage, return the blob from the database. - return resource.Blob, nil + return attachment.Blob, nil } const ( @@ -480,22 +480,22 @@ const ( thumbnailRatio = 0.8 ) -// getOrGenerateThumbnail returns the thumbnail image of the resource. -func (s *APIV1Service) getOrGenerateThumbnail(resource *store.Resource) ([]byte, error) { +// getOrGenerateThumbnail returns the thumbnail image of the attachment. +func (s *APIV1Service) getOrGenerateThumbnail(attachment *store.Attachment) ([]byte, error) { thumbnailCacheFolder := filepath.Join(s.Profile.Data, ThumbnailCacheFolder) if err := os.MkdirAll(thumbnailCacheFolder, os.ModePerm); err != nil { return nil, errors.Wrap(err, "failed to create thumbnail cache folder") } - filePath := filepath.Join(thumbnailCacheFolder, fmt.Sprintf("%d%s", resource.ID, filepath.Ext(resource.Filename))) + filePath := filepath.Join(thumbnailCacheFolder, fmt.Sprintf("%d%s", attachment.ID, filepath.Ext(attachment.Filename))) if _, err := os.Stat(filePath); err != nil { if !os.IsNotExist(err) { return nil, errors.Wrap(err, "failed to check thumbnail image stat") } // If thumbnail image does not exist, generate and save the thumbnail image. - blob, err := s.GetResourceBlob(resource) + blob, err := s.GetAttachmentBlob(attachment) if err != nil { - return nil, errors.Wrap(err, "failed to get resource blob") + return nil, errors.Wrap(err, "failed to get attachment blob") } img, err := imaging.Decode(bytes.NewReader(blob), imaging.AutoOrientation(true)) if err != nil { diff --git a/server/router/api/v1/memo_attachment_service.go b/server/router/api/v1/memo_attachment_service.go index 1be68d688..95e0d480b 100644 --- a/server/router/api/v1/memo_attachment_service.go +++ b/server/router/api/v1/memo_attachment_service.go @@ -22,54 +22,54 @@ func (s *APIV1Service) SetMemoAttachments(ctx context.Context, request *v1pb.Set if err != nil { 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, }) 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. - for _, resource := range resources { + // Delete attachments that are not in the request. + for _, attachment := range attachments { found := false - for _, requestResource := range request.Attachments { - requestResourceUID, err := ExtractAttachmentUIDFromName(requestResource.Name) + for _, requestAttachment := range request.Attachments { + requestAttachmentUID, err := ExtractAttachmentUIDFromName(requestAttachment.Name) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "invalid attachment name: %v", err) } - if resource.UID == requestResourceUID { + if attachment.UID == requestAttachmentUID { found = true break } } if !found { - if err = s.Store.DeleteResource(ctx, &store.DeleteResource{ - ID: int32(resource.ID), + if err = s.Store.DeleteAttachment(ctx, &store.DeleteAttachment{ + ID: int32(attachment.ID), MemoID: &memo.ID, }); 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) - // Update resources' memo_id in the request. - for index, resource := range request.Attachments { - resourceUID, err := ExtractAttachmentUIDFromName(resource.Name) + // Update attachments' memo_id in the request. + for index, attachment := range request.Attachments { + attachmentUID, err := ExtractAttachmentUIDFromName(attachment.Name) if err != nil { 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 { - 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) - if err := s.Store.UpdateResource(ctx, &store.UpdateResource{ - ID: tempResource.ID, + if err := s.Store.UpdateAttachment(ctx, &store.UpdateAttachment{ + ID: tempAttachment.ID, MemoID: &memo.ID, UpdatedTs: &updatedTs, }); 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 if err != nil { 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, }) 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{ Attachments: []*v1pb.Attachment{}, } - for _, resource := range resources { - response.Attachments = append(response.Attachments, s.convertAttachmentFromStore(ctx, resource)) + for _, attachment := range attachments { + response.Attachments = append(response.Attachments, s.convertAttachmentFromStore(ctx, attachment)) } return response, nil } diff --git a/server/router/api/v1/memo_service.go b/server/router/api/v1/memo_service.go index 571bef8d4..9c9ea9bae 100644 --- a/server/router/api/v1/memo_service.go +++ b/server/router/api/v1/memo_service.go @@ -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") } - // Delete related resources. - resources, err := s.Store.ListResources(ctx, &store.FindResource{MemoID: &memo.ID}) + // Delete related attachments. + attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{MemoID: &memo.ID}) 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 { - if err := s.Store.DeleteResource(ctx, &store.DeleteResource{ID: resource.ID}); err != nil { - return nil, status.Errorf(codes.Internal, "failed to delete resource") + for _, attachment := range attachments { + if err := s.Store.DeleteAttachment(ctx, &store.DeleteAttachment{ID: attachment.ID}); err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete attachment") } } diff --git a/server/router/rss/rss.go b/server/router/rss/rss.go index 1d3281cd4..42b42aa41 100644 --- a/server/router/rss/rss.go +++ b/server/router/rss/rss.go @@ -124,22 +124,22 @@ func (s *RSSService) generateRSSFromMemoList(ctx context.Context, memoList []*st Created: time.Unix(memo.CreatedTs, 0), Id: link.Href, } - resources, err := s.Store.ListResources(ctx, &store.FindResource{ + attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{ MemoID: &memo.ID, }) if err != nil { return "", err } - if len(resources) > 0 { - resource := resources[0] + if len(attachments) > 0 { + attachment := attachments[0] enclosure := feeds.Enclosure{} - if resource.StorageType == storepb.ResourceStorageType_EXTERNAL || resource.StorageType == storepb.ResourceStorageType_S3 { - enclosure.Url = resource.Reference + if attachment.StorageType == storepb.AttachmentStorageType_EXTERNAL || attachment.StorageType == storepb.AttachmentStorageType_S3 { + enclosure.Url = attachment.Reference } 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.Type = resource.Type + enclosure.Length = strconv.Itoa(int(attachment.Size)) + enclosure.Type = attachment.Type feed.Items[i].Enclosure = &enclosure } } diff --git a/server/runner/s3presign/runner.go b/server/runner/s3presign/runner.go index 085d2b670..582aec1ca 100644 --- a/server/runner/s3presign/runner.go +++ b/server/runner/s3presign/runner.go @@ -49,33 +49,33 @@ func (r *Runner) CheckAndPresign(ctx context.Context) { return } - s3StorageType := storepb.ResourceStorageType_S3 - // Limit resources to a reasonable batch size + s3StorageType := storepb.AttachmentStorageType_S3 + // Limit attachments to a reasonable batch size const batchSize = 100 offset := 0 for { limit := batchSize - resources, err := r.Store.ListResources(ctx, &store.FindResource{ + attachments, err := r.Store.ListAttachments(ctx, &store.FindAttachment{ GetBlob: false, StorageType: &s3StorageType, Limit: &limit, Offset: &offset, }) if err != nil { - slog.Error("Failed to list resources for presigning", "error", err) + slog.Error("Failed to list attachments for presigning", "error", err) return } - // Break if no more resources - if len(resources) == 0 { + // Break if no more attachments + if len(attachments) == 0 { break } - // Process batch of resources + // Process batch of attachments presignCount := 0 - for _, resource := range resources { - s3ObjectPayload := resource.Payload.GetS3Object() + for _, attachment := range attachments { + s3ObjectPayload := attachment.Payload.GetS3Object() if s3ObjectPayload == nil { continue } @@ -105,30 +105,30 @@ func (r *Runner) CheckAndPresign(ctx context.Context) { presignURL, err := s3Client.PresignGetObject(ctx, s3ObjectPayload.Key) 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 } s3ObjectPayload.S3Config = s3Config s3ObjectPayload.LastPresignedTime = timestamppb.New(time.Now()) - if err := r.Store.UpdateResource(ctx, &store.UpdateResource{ - ID: resource.ID, + if err := r.Store.UpdateAttachment(ctx, &store.UpdateAttachment{ + ID: attachment.ID, Reference: &presignURL, - Payload: &storepb.ResourcePayload{ - Payload: &storepb.ResourcePayload_S3Object_{ + Payload: &storepb.AttachmentPayload{ + Payload: &storepb.AttachmentPayload_S3Object_{ S3Object: s3ObjectPayload, }, }, }); 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 } 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 - offset += len(resources) + offset += len(attachments) } } diff --git a/store/resource.go b/store/attachment.go similarity index 61% rename from store/resource.go rename to store/attachment.go index 7d6564fcf..acbc1777e 100644 --- a/store/resource.go +++ b/store/attachment.go @@ -13,10 +13,10 @@ import ( storepb "github.com/usememos/memos/proto/gen/store" ) -type Resource struct { - // ID is the system generated unique identifier for the resource. +type Attachment struct { + // ID is the system generated unique identifier for the attachment. ID int32 - // UID is the user defined unique identifier for the resource. + // UID is the user defined unique identifier for the attachment. UID string // Standard fields @@ -29,15 +29,15 @@ type Resource struct { Blob []byte Type string Size int64 - StorageType storepb.ResourceStorageType + StorageType storepb.AttachmentStorageType Reference string - Payload *storepb.ResourcePayload + Payload *storepb.AttachmentPayload // The related memo ID. MemoID *int32 } -type FindResource struct { +type FindAttachment struct { GetBlob bool ID *int32 UID *string @@ -46,35 +46,35 @@ type FindResource struct { FilenameSearch *string MemoID *int32 HasRelatedMemo bool - StorageType *storepb.ResourceStorageType + StorageType *storepb.AttachmentStorageType Limit *int Offset *int } -type UpdateResource struct { +type UpdateAttachment struct { ID int32 UID *string UpdatedTs *int64 Filename *string MemoID *int32 Reference *string - Payload *storepb.ResourcePayload + Payload *storepb.AttachmentPayload } -type DeleteResource struct { +type DeleteAttachment struct { ID 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) { 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) { - // Set default limits to prevent loading too many resources at once +func (s *Store) ListAttachments(ctx context.Context, find *FindAttachment) ([]*Attachment, error) { + // Set default limits to prevent loading too many attachments at once if find.Limit == nil && find.GetBlob { // When fetching blobs, we should be especially careful with limits defaultLimit := 10 @@ -85,41 +85,41 @@ func (s *Store) ListResources(ctx context.Context, find *FindResource) ([]*Resou 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) { - resources, err := s.ListResources(ctx, find) +func (s *Store) GetAttachment(ctx context.Context, find *FindAttachment) (*Attachment, error) { + attachments, err := s.ListAttachments(ctx, find) if err != nil { return nil, err } - if len(resources) == 0 { + if len(attachments) == 0 { 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) { 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 { - resource, err := s.GetResource(ctx, &FindResource{ID: &delete.ID}) +func (s *Store) DeleteAttachment(ctx context.Context, delete *DeleteAttachment) error { + attachment, err := s.GetAttachment(ctx, &FindAttachment{ID: &delete.ID}) if err != nil { - return errors.Wrap(err, "failed to get resource") + return errors.Wrap(err, "failed to get attachment") } - if resource == nil { - return errors.New("resource not found") + if attachment == nil { + return errors.New("attachment not found") } - if resource.StorageType == storepb.ResourceStorageType_LOCAL { + if attachment.StorageType == storepb.AttachmentStorageType_LOCAL { if err := func() error { - p := filepath.FromSlash(resource.Reference) + p := filepath.FromSlash(attachment.Reference) if !filepath.IsAbs(p) { p = filepath.Join(s.profile.Data, p) } @@ -131,9 +131,9 @@ func (s *Store) DeleteResource(ctx context.Context, delete *DeleteResource) erro }(); err != nil { 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 { - s3ObjectPayload := resource.Payload.GetS3Object() + s3ObjectPayload := attachment.Payload.GetS3Object() if s3ObjectPayload == nil { return errors.Errorf("No s3 object found") } @@ -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) } diff --git a/store/db/mysql/resource.go b/store/db/mysql/attachment.go similarity index 76% rename from store/db/mysql/resource.go rename to store/db/mysql/attachment.go index 004860136..468e9032e 100644 --- a/store/db/mysql/resource.go +++ b/store/db/mysql/attachment.go @@ -13,18 +13,18 @@ import ( "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`"} placeholder := []string{"?", "?", "?", "?", "?", "?", "?", "?", "?", "?"} storageType := "" - if create.StorageType != storepb.ResourceStorageType_RESOURCE_STORAGE_TYPE_UNSPECIFIED { + if create.StorageType != storepb.AttachmentStorageType_ATTACHMENT_STORAGE_TYPE_UNSPECIFIED { storageType = create.StorageType.String() } payloadString := "{}" if create.Payload != nil { bytes, err := protojson.Marshal(create.Payload) 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) } @@ -42,10 +42,10 @@ func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store } 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{} if v := find.ID; v != nil { @@ -92,43 +92,43 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st } defer rows.Close() - list := make([]*store.Resource, 0) + list := make([]*store.Attachment, 0) for rows.Next() { - resource := store.Resource{} + attachment := store.Attachment{} var memoID sql.NullInt32 var storageType string var payloadBytes []byte dests := []any{ - &resource.ID, - &resource.UID, - &resource.Filename, - &resource.Type, - &resource.Size, - &resource.CreatorID, - &resource.CreatedTs, - &resource.UpdatedTs, + &attachment.ID, + &attachment.UID, + &attachment.Filename, + &attachment.Type, + &attachment.Size, + &attachment.CreatorID, + &attachment.CreatedTs, + &attachment.UpdatedTs, &memoID, &storageType, - &resource.Reference, + &attachment.Reference, &payloadBytes, } if find.GetBlob { - dests = append(dests, &resource.Blob) + dests = append(dests, &attachment.Blob) } if err := rows.Scan(dests...); err != nil { return nil, err } if memoID.Valid { - resource.MemoID = &memoID.Int32 + attachment.MemoID = &memoID.Int32 } - resource.StorageType = storepb.ResourceStorageType(storepb.ResourceStorageType_value[storageType]) - payload := &storepb.ResourcePayload{} + attachment.StorageType = storepb.AttachmentStorageType(storepb.AttachmentStorageType_value[storageType]) + payload := &storepb.AttachmentPayload{} if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil { return nil, err } - resource.Payload = payload - list = append(list, &resource) + attachment.Payload = payload + list = append(list, &attachment) } if err := rows.Err(); err != nil { @@ -138,8 +138,8 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st return list, nil } -func (d *DB) GetResource(ctx context.Context, find *store.FindResource) (*store.Resource, error) { - list, err := d.ListResources(ctx, find) +func (d *DB) GetAttachment(ctx context.Context, find *store.FindAttachment) (*store.Attachment, error) { + list, err := d.ListAttachments(ctx, find) if err != nil { return nil, err } @@ -150,7 +150,7 @@ func (d *DB) GetResource(ctx context.Context, find *store.FindResource) (*store. 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{} if v := update.UID; v != nil { @@ -171,7 +171,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e if v := update.Payload; v != nil { bytes, err := protojson.Marshal(v) 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)) } @@ -188,7 +188,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e 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` = ?" result, err := d.db.ExecContext(ctx, stmt, delete.ID) if err != nil { diff --git a/store/db/postgres/resource.go b/store/db/postgres/attachment.go similarity index 79% rename from store/db/postgres/resource.go rename to store/db/postgres/attachment.go index e577b7aee..da24c3710 100644 --- a/store/db/postgres/resource.go +++ b/store/db/postgres/attachment.go @@ -13,17 +13,17 @@ import ( "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"} storageType := "" - if create.StorageType != storepb.ResourceStorageType_RESOURCE_STORAGE_TYPE_UNSPECIFIED { + if create.StorageType != storepb.AttachmentStorageType_ATTACHMENT_STORAGE_TYPE_UNSPECIFIED { storageType = create.StorageType.String() } payloadString := "{}" if create.Payload != nil { bytes, err := protojson.Marshal(create.Payload) 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) } @@ -36,7 +36,7 @@ func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store 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{} if v := find.ID; v != nil { @@ -89,43 +89,43 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st } defer rows.Close() - list := make([]*store.Resource, 0) + list := make([]*store.Attachment, 0) for rows.Next() { - resource := store.Resource{} + attachment := store.Attachment{} var memoID sql.NullInt32 var storageType string var payloadBytes []byte dests := []any{ - &resource.ID, - &resource.UID, - &resource.Filename, - &resource.Type, - &resource.Size, - &resource.CreatorID, - &resource.CreatedTs, - &resource.UpdatedTs, + &attachment.ID, + &attachment.UID, + &attachment.Filename, + &attachment.Type, + &attachment.Size, + &attachment.CreatorID, + &attachment.CreatedTs, + &attachment.UpdatedTs, &memoID, &storageType, - &resource.Reference, + &attachment.Reference, &payloadBytes, } if find.GetBlob { - dests = append(dests, &resource.Blob) + dests = append(dests, &attachment.Blob) } if err := rows.Scan(dests...); err != nil { return nil, err } if memoID.Valid { - resource.MemoID = &memoID.Int32 + attachment.MemoID = &memoID.Int32 } - resource.StorageType = storepb.ResourceStorageType(storepb.ResourceStorageType_value[storageType]) - payload := &storepb.ResourcePayload{} + attachment.StorageType = storepb.AttachmentStorageType(storepb.AttachmentStorageType_value[storageType]) + payload := &storepb.AttachmentPayload{} if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil { return nil, err } - resource.Payload = payload - list = append(list, &resource) + attachment.Payload = payload + list = append(list, &attachment) } if err := rows.Err(); err != nil { @@ -135,7 +135,7 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st 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{} if v := update.UID; v != nil { @@ -156,7 +156,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e if v := update.Payload; v != nil { bytes, err := protojson.Marshal(v) 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)) } @@ -173,7 +173,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e 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` result, err := d.db.ExecContext(ctx, stmt, delete.ID) if err != nil { diff --git a/store/db/sqlite/resource.go b/store/db/sqlite/attachment.go similarity index 77% rename from store/db/sqlite/resource.go rename to store/db/sqlite/attachment.go index 6e16f28bc..ee547b8f4 100644 --- a/store/db/sqlite/resource.go +++ b/store/db/sqlite/attachment.go @@ -13,18 +13,18 @@ import ( "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`"} placeholder := []string{"?", "?", "?", "?", "?", "?", "?", "?", "?", "?"} storageType := "" - if create.StorageType != storepb.ResourceStorageType_RESOURCE_STORAGE_TYPE_UNSPECIFIED { + if create.StorageType != storepb.AttachmentStorageType_ATTACHMENT_STORAGE_TYPE_UNSPECIFIED { storageType = create.StorageType.String() } payloadString := "{}" if create.Payload != nil { bytes, err := protojson.Marshal(create.Payload) 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) } @@ -38,7 +38,7 @@ func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store 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{} if v := find.ID; v != nil { @@ -85,43 +85,43 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st } defer rows.Close() - list := make([]*store.Resource, 0) + list := make([]*store.Attachment, 0) for rows.Next() { - resource := store.Resource{} + attachment := store.Attachment{} var memoID sql.NullInt32 var storageType string var payloadBytes []byte dests := []any{ - &resource.ID, - &resource.UID, - &resource.Filename, - &resource.Type, - &resource.Size, - &resource.CreatorID, - &resource.CreatedTs, - &resource.UpdatedTs, + &attachment.ID, + &attachment.UID, + &attachment.Filename, + &attachment.Type, + &attachment.Size, + &attachment.CreatorID, + &attachment.CreatedTs, + &attachment.UpdatedTs, &memoID, &storageType, - &resource.Reference, + &attachment.Reference, &payloadBytes, } if find.GetBlob { - dests = append(dests, &resource.Blob) + dests = append(dests, &attachment.Blob) } if err := rows.Scan(dests...); err != nil { return nil, err } if memoID.Valid { - resource.MemoID = &memoID.Int32 + attachment.MemoID = &memoID.Int32 } - resource.StorageType = storepb.ResourceStorageType(storepb.ResourceStorageType_value[storageType]) - payload := &storepb.ResourcePayload{} + attachment.StorageType = storepb.AttachmentStorageType(storepb.AttachmentStorageType_value[storageType]) + payload := &storepb.AttachmentPayload{} if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil { return nil, err } - resource.Payload = payload - list = append(list, &resource) + attachment.Payload = payload + list = append(list, &attachment) } if err := rows.Err(); err != nil { @@ -131,7 +131,7 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st 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{} if v := update.UID; v != nil { @@ -152,7 +152,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e if v := update.Payload; v != nil { bytes, err := protojson.Marshal(v) 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)) } @@ -161,7 +161,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e stmt := "UPDATE `resource` SET " + strings.Join(set, ", ") + " WHERE `id` = ?" result, err := d.db.ExecContext(ctx, stmt, args...) 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 { return err @@ -169,7 +169,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e 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` = ?" result, err := d.db.ExecContext(ctx, stmt, delete.ID) if err != nil { diff --git a/store/driver.go b/store/driver.go index 27f110034..da893e744 100644 --- a/store/driver.go +++ b/store/driver.go @@ -25,11 +25,11 @@ type Driver interface { CreateActivity(ctx context.Context, create *Activity) (*Activity, error) ListActivities(ctx context.Context, find *FindActivity) ([]*Activity, error) - // Resource model related methods. - CreateResource(ctx context.Context, create *Resource) (*Resource, error) - ListResources(ctx context.Context, find *FindResource) ([]*Resource, error) - UpdateResource(ctx context.Context, update *UpdateResource) error - DeleteResource(ctx context.Context, delete *DeleteResource) error + // Attachment model related methods. + CreateAttachment(ctx context.Context, create *Attachment) (*Attachment, error) + ListAttachments(ctx context.Context, find *FindAttachment) ([]*Attachment, error) + UpdateAttachment(ctx context.Context, update *UpdateAttachment) error + DeleteAttachment(ctx context.Context, delete *DeleteAttachment) error // Memo model related methods. CreateMemo(ctx context.Context, create *Memo) (*Memo, error) diff --git a/store/test/resource_test.go b/store/test/attachment_test.go similarity index 55% rename from store/test/resource_test.go rename to store/test/attachment_test.go index dcf6751bf..a653a0d97 100644 --- a/store/test/resource_test.go +++ b/store/test/attachment_test.go @@ -10,10 +10,10 @@ import ( "github.com/usememos/memos/store" ) -func TestResourceStore(t *testing.T) { +func TestAttachmentStore(t *testing.T) { ctx := context.Background() ts := NewTestingStore(ctx, t) - _, err := ts.CreateResource(ctx, &store.Resource{ + _, err := ts.CreateAttachment(ctx, &store.Attachment{ UID: shortuuid.New(), CreatorID: 101, Filename: "test.epub", @@ -25,39 +25,39 @@ func TestResourceStore(t *testing.T) { correctFilename := "test.epub" incorrectFilename := "test.png" - resource, err := ts.GetResource(ctx, &store.FindResource{ + attachment, err := ts.GetAttachment(ctx, &store.FindAttachment{ Filename: &correctFilename, }) require.NoError(t, err) - require.Equal(t, correctFilename, resource.Filename) - require.Equal(t, int32(1), resource.ID) + require.Equal(t, correctFilename, attachment.Filename) + require.Equal(t, int32(1), attachment.ID) - notFoundResource, err := ts.GetResource(ctx, &store.FindResource{ + notFoundAttachment, err := ts.GetAttachment(ctx, &store.FindAttachment{ Filename: &incorrectFilename, }) require.NoError(t, err) - require.Nil(t, notFoundResource) + require.Nil(t, notFoundAttachment) var correctCreatorID int32 = 101 var incorrectCreatorID int32 = 102 - _, err = ts.GetResource(ctx, &store.FindResource{ + _, err = ts.GetAttachment(ctx, &store.FindAttachment{ CreatorID: &correctCreatorID, }) require.NoError(t, err) - notFoundResource, err = ts.GetResource(ctx, &store.FindResource{ + notFoundAttachment, err = ts.GetAttachment(ctx, &store.FindAttachment{ CreatorID: &incorrectCreatorID, }) 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, }) require.NoError(t, err) - err = ts.DeleteResource(ctx, &store.DeleteResource{ + err = ts.DeleteAttachment(ctx, &store.DeleteAttachment{ ID: 2, }) - require.ErrorContains(t, err, "resource not found") + require.ErrorContains(t, err, "attachment not found") ts.Close() } diff --git a/web/src/components/ResourceIcon.tsx b/web/src/components/AttachmentIcon.tsx similarity index 85% rename from web/src/components/ResourceIcon.tsx rename to web/src/components/AttachmentIcon.tsx index 19f2b6a27..02153a3e1 100644 --- a/web/src/components/ResourceIcon.tsx +++ b/web/src/components/AttachmentIcon.tsx @@ -17,15 +17,15 @@ import showPreviewImageDialog from "./PreviewImageDialog"; import SquareDiv from "./kit/SquareDiv"; interface Props { - resource: Attachment; + attachment: Attachment; className?: string; strokeWidth?: number; } -const ResourceIcon = (props: Props) => { - const { resource } = props; - const resourceType = getAttachmentType(resource); - const resourceUrl = getAttachmentUrl(resource); +const AttachmentIcon = (props: Props) => { + const { attachment } = props; + const resourceType = getAttachmentType(attachment); + const resourceUrl = getAttachmentUrl(attachment); const className = cn("w-full h-auto", props.className); const strokeWidth = props.strokeWidth; @@ -38,7 +38,7 @@ const ResourceIcon = (props: Props) => { showPreviewImageDialog(resourceUrl)} decoding="async" loading="lazy" @@ -47,7 +47,7 @@ const ResourceIcon = (props: Props) => { ); } - const getResourceIcon = () => { + const getAttachmentIcon = () => { switch (resourceType) { case "video/*": return ; @@ -74,9 +74,9 @@ const ResourceIcon = (props: Props) => { return (
- {getResourceIcon()} + {getAttachmentIcon()}
); }; -export default React.memo(ResourceIcon); +export default React.memo(AttachmentIcon); diff --git a/web/src/components/MemoAttachment.tsx b/web/src/components/MemoAttachment.tsx new file mode 100644 index 000000000..d24bee9f5 --- /dev/null +++ b/web/src/components/MemoAttachment.tsx @@ -0,0 +1,34 @@ +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) => { + const { className, attachment } = props; + const attachmentUrl = getAttachmentUrl(attachment); + + const handlePreviewBtnClick = () => { + window.open(attachmentUrl); + }; + + return ( +
+ {attachment.type.startsWith("audio") ? ( + + ) : ( + <> + + + {attachment.filename} + + + )} +
+ ); +}; + +export default MemoAttachment; diff --git a/web/src/components/MemoAttachmentListView.tsx b/web/src/components/MemoAttachmentListView.tsx index f8b4e51c2..7f830e92f 100644 --- a/web/src/components/MemoAttachmentListView.tsx +++ b/web/src/components/MemoAttachmentListView.tsx @@ -2,7 +2,7 @@ import { memo } from "react"; import { Attachment } from "@/types/proto/api/v1/attachment_service"; import { cn } from "@/utils"; import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment"; -import MemoResource from "./MemoResource"; +import MemoAttachment from "./MemoAttachment"; import showPreviewImageDialog from "./PreviewImageDialog"; const MemoAttachmentListView = ({ attachments = [] }: { attachments: Attachment[] }) => { @@ -78,7 +78,7 @@ const MemoAttachmentListView = ({ attachments = [] }: { attachments: Attachment[ return (
{otherAttachments.map((attachment) => ( - + ))}
); diff --git a/web/src/components/MemoEditor/ActionButton/UploadAttachmentButton.tsx b/web/src/components/MemoEditor/ActionButton/UploadAttachmentButton.tsx new file mode 100644 index 000000000..6aab03092 --- /dev/null +++ b/web/src/components/MemoEditor/ActionButton/UploadAttachmentButton.tsx @@ -0,0 +1,92 @@ +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({ + uploadingFlag: false, + }); + const fileInputRef = useRef(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 ( + + ); +}); + +export default UploadAttachmentButton; diff --git a/web/src/components/MemoEditor/ActionButton/UploadResourceButton.tsx b/web/src/components/MemoEditor/ActionButton/UploadResourceButton.tsx index f7a1b98e3..e69de29bb 100644 --- a/web/src/components/MemoEditor/ActionButton/UploadResourceButton.tsx +++ b/web/src/components/MemoEditor/ActionButton/UploadResourceButton.tsx @@ -1,92 +0,0 @@ -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({ - uploadingFlag: false, - }); - const fileInputRef = useRef(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 ( - - ); -}); - -export default UploadResourceButton; diff --git a/web/src/components/MemoEditor/AttachmentListView.tsx b/web/src/components/MemoEditor/AttachmentListView.tsx index dbe365396..264840d57 100644 --- a/web/src/components/MemoEditor/AttachmentListView.tsx +++ b/web/src/components/MemoEditor/AttachmentListView.tsx @@ -2,7 +2,7 @@ import { DndContext, closestCenter, MouseSensor, TouchSensor, useSensor, useSens import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; import { XIcon } from "lucide-react"; import { Attachment } from "@/types/proto/api/v1/attachment_service"; -import ResourceIcon from "../ResourceIcon"; +import AttachmentIcon from "../AttachmentIcon"; import SortableItem from "./SortableItem"; interface 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" > - + {attachment.filename}