refactor: attachment service part2

pull/4778/head
Steven 5 months ago
parent bb5809cae4
commit a4920d464b

@ -1,4 +1 @@
// Package httpgetter is using to get resources from url.
// * Get metadata for website;
// * Get image blob to avoid CORS;
package httpgetter

@ -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
}

@ -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
}

@ -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;
}

@ -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.

@ -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: &currentTs,
}
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 {

@ -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
}

@ -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")
}
}

@ -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
}
}

@ -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)
}
}

@ -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)
}

@ -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 {

@ -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 {

@ -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 {

@ -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)

@ -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()
}

@ -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) => {
<SquareDiv className={cn(className, "flex items-center justify-center overflow-clip")}>
<img
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)}
decoding="async"
loading="lazy"
@ -47,7 +47,7 @@ const ResourceIcon = (props: Props) => {
);
}
const getResourceIcon = () => {
const getAttachmentIcon = () => {
switch (resourceType) {
case "video/*":
return <FileVideo2Icon strokeWidth={strokeWidth} className="w-full h-auto" />;
@ -74,9 +74,9 @@ const ResourceIcon = (props: Props) => {
return (
<div onClick={previewResource} className={cn(className, "max-w-16 opacity-50")}>
{getResourceIcon()}
{getAttachmentIcon()}
</div>
);
};
export default React.memo(ResourceIcon);
export default React.memo(AttachmentIcon);

@ -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: 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";
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 (
<div className="w-full flex flex-row justify-start overflow-auto gap-2">
{otherAttachments.map((attachment) => (
<MemoResource key={attachment.name} resource={attachment} />
<MemoAttachment key={attachment.name} attachment={attachment} />
))}
</div>
);

@ -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<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;

@ -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<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
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"
>
<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>
</SortableItem>
<button className="shrink-0" onClick={() => handleDeleteAttachment(attachment.name)}>

@ -25,7 +25,7 @@ import AddMemoRelationPopover from "./ActionButton/AddMemoRelationPopover";
import LocationSelector from "./ActionButton/LocationSelector";
import MarkdownMenu from "./ActionButton/MarkdownMenu";
import TagSelector from "./ActionButton/TagSelector";
import UploadResourceButton from "./ActionButton/UploadResourceButton";
import UploadAttachmentButton from "./ActionButton/UploadAttachmentButton";
import VisibilitySelector from "./ActionButton/VisibilitySelector";
import AttachmentListView from "./AttachmentListView";
import Editor, { EditorRefActions } from "./Editor";
@ -51,7 +51,7 @@ interface State {
attachmentList: Attachment[];
relationList: MemoRelation[];
location: Location | undefined;
isUploadingResource: boolean;
isUploadingAttachment: boolean;
isRequesting: boolean;
isComposing: boolean;
isDraggingFile: boolean;
@ -67,7 +67,7 @@ const MemoEditor = observer((props: Props) => {
attachmentList: [],
relationList: [],
location: undefined,
isUploadingResource: false,
isUploadingAttachment: false,
isRequesting: false,
isComposing: false,
isDraggingFile: false,
@ -203,7 +203,7 @@ const MemoEditor = observer((props: Props) => {
setState((state) => {
return {
...state,
isUploadingResource: true,
isUploadingAttachment: true,
};
});
@ -223,7 +223,7 @@ const MemoEditor = observer((props: Props) => {
setState((state) => {
return {
...state,
isUploadingResource: false,
isUploadingAttachment: false,
};
});
return attachment;
@ -233,7 +233,7 @@ const MemoEditor = observer((props: Props) => {
setState((state) => {
return {
...state,
isUploadingResource: false,
isUploadingAttachment: false,
};
});
}
@ -456,7 +456,7 @@ const MemoEditor = observer((props: Props) => {
[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 (
<MemoEditorContext.Provider
@ -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">
<TagSelector editorRef={editorRef} />
<MarkdownMenu editorRef={editorRef} />
<UploadResourceButton isUploadingResource={state.isUploadingResource} />
<UploadAttachmentButton isUploading={state.isUploadingAttachment} />
<AddMemoRelationPopover editorRef={editorRef} />
<LocationSelector
location={state.location}

@ -1,34 +0,0 @@
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";
import { PaperclipIcon, SearchIcon, TrashIcon } from "lucide-react";
import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react";
import AttachmentIcon from "@/components/AttachmentIcon";
import Empty from "@/components/Empty";
import MobileHeader from "@/components/MobileHeader";
import ResourceIcon from "@/components/ResourceIcon";
import { attachmentServiceClient } from "@/grpcweb";
import useLoading from "@/hooks/useLoading";
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
@ -112,7 +112,7 @@ const Attachments = observer(() => {
return (
<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">
<ResourceIcon resource={attachment} strokeWidth={0.5} />
<AttachmentIcon attachment={attachment} strokeWidth={0.5} />
</div>
<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>
@ -144,7 +144,7 @@ const Attachments = observer(() => {
return (
<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">
<ResourceIcon resource={attachment} strokeWidth={0.5} />
<AttachmentIcon attachment={attachment} strokeWidth={0.5} />
</div>
<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>

Loading…
Cancel
Save