mirror of https://github.com/usememos/memos.git
refactor: attachment service part2
This commit is contained in:
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
|
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";
|
option go_package = "gen/store";
|
||||||
|
|
||||||
enum ResourceStorageType {
|
enum AttachmentStorageType {
|
||||||
RESOURCE_STORAGE_TYPE_UNSPECIFIED = 0;
|
ATTACHMENT_STORAGE_TYPE_UNSPECIFIED = 0;
|
||||||
// Resource is stored locally. AKA, local file system.
|
// Attachment is stored locally. AKA, local file system.
|
||||||
LOCAL = 1;
|
LOCAL = 1;
|
||||||
// Resource is stored in S3.
|
// Attachment is stored in S3.
|
||||||
S3 = 2;
|
S3 = 2;
|
||||||
// Resource is stored in an external storage. The reference is a URL.
|
// Attachment is stored in an external storage. The reference is a URL.
|
||||||
EXTERNAL = 3;
|
EXTERNAL = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ResourcePayload {
|
message AttachmentPayload {
|
||||||
oneof payload {
|
oneof payload {
|
||||||
S3Object s3_object = 1;
|
S3Object s3_object = 1;
|
||||||
}
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ var authenticationAllowlistMethods = map[string]bool{
|
||||||
"/memos.api.v1.MemoService/GetMemo": true,
|
"/memos.api.v1.MemoService/GetMemo": true,
|
||||||
"/memos.api.v1.MemoService/ListMemos": true,
|
"/memos.api.v1.MemoService/ListMemos": true,
|
||||||
"/memos.api.v1.MarkdownService/GetLinkMetadata": true,
|
"/memos.api.v1.MarkdownService/GetLinkMetadata": true,
|
||||||
"/memos.api.v1.ResourceService/GetResourceBinary": true,
|
"/memos.api.v1.AttachmentService/GetAttachmentBinary": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// isUnauthorizeAllowedMethod returns whether the method is exempted from authentication.
|
// isUnauthorizeAllowedMethod returns whether the method is exempted from authentication.
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.Creat
|
||||||
attachmentUID = shortuuid.New()
|
attachmentUID = shortuuid.New()
|
||||||
}
|
}
|
||||||
|
|
||||||
create := &store.Resource{
|
create := &store.Attachment{
|
||||||
UID: attachmentUID,
|
UID: attachmentUID,
|
||||||
CreatorID: user.ID,
|
CreatorID: user.ID,
|
||||||
Filename: request.Attachment.Filename,
|
Filename: request.Attachment.Filename,
|
||||||
|
|
@ -90,8 +90,8 @@ func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.Creat
|
||||||
create.Size = int64(size)
|
create.Size = int64(size)
|
||||||
create.Blob = request.Attachment.Content
|
create.Blob = request.Attachment.Content
|
||||||
|
|
||||||
if err := SaveResourceBlob(ctx, s.Profile, s.Store, create); err != nil {
|
if err := SaveAttachmentBlob(ctx, s.Profile, s.Store, create); err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "failed to save resource blob: %v", err)
|
return nil, status.Errorf(codes.Internal, "failed to save attachment blob: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.Attachment.Memo != nil {
|
if request.Attachment.Memo != nil {
|
||||||
|
|
@ -108,12 +108,12 @@ func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.Creat
|
||||||
}
|
}
|
||||||
create.MemoID = &memo.ID
|
create.MemoID = &memo.ID
|
||||||
}
|
}
|
||||||
resource, err := s.Store.CreateResource(ctx, create)
|
attachment, err := s.Store.CreateAttachment(ctx, create)
|
||||||
if err != nil {
|
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) {
|
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,
|
CreatorID: &user.ID,
|
||||||
Limit: &pageSize,
|
Limit: &pageSize,
|
||||||
Offset: &offset,
|
Offset: &offset,
|
||||||
|
|
@ -154,40 +154,40 @@ func (s *APIV1Service) ListAttachments(ctx context.Context, request *v1pb.ListAt
|
||||||
if strings.HasPrefix(request.Filter, "type=") {
|
if strings.HasPrefix(request.Filter, "type=") {
|
||||||
filterType := strings.TrimPrefix(request.Filter, "type=")
|
filterType := strings.TrimPrefix(request.Filter, "type=")
|
||||||
// Create a temporary struct to hold type filter
|
// 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
|
_ = filterType // We'll filter after getting results
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resources, err := s.Store.ListResources(ctx, findResource)
|
attachments, err := s.Store.ListAttachments(ctx, findAttachment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "failed to list resources: %v", err)
|
return nil, status.Errorf(codes.Internal, "failed to list attachments: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply type filter if specified
|
// Apply type filter if specified
|
||||||
if request.Filter != "" && strings.HasPrefix(request.Filter, "type=") {
|
if request.Filter != "" && strings.HasPrefix(request.Filter, "type=") {
|
||||||
filterType := strings.TrimPrefix(request.Filter, "type=")
|
filterType := strings.TrimPrefix(request.Filter, "type=")
|
||||||
filteredResources := make([]*store.Resource, 0)
|
filteredAttachments := make([]*store.Attachment, 0)
|
||||||
for _, resource := range resources {
|
for _, attachment := range attachments {
|
||||||
if resource.Type == filterType {
|
if attachment.Type == filterType {
|
||||||
filteredResources = append(filteredResources, resource)
|
filteredAttachments = append(filteredAttachments, attachment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resources = filteredResources
|
attachments = filteredAttachments
|
||||||
}
|
}
|
||||||
|
|
||||||
response := &v1pb.ListAttachmentsResponse{}
|
response := &v1pb.ListAttachmentsResponse{}
|
||||||
|
|
||||||
for _, resource := range resources {
|
for _, attachment := range attachments {
|
||||||
response.Attachments = append(response.Attachments, s.convertAttachmentFromStore(ctx, resource))
|
response.Attachments = append(response.Attachments, s.convertAttachmentFromStore(ctx, attachment))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// In a full implementation, you'd want a separate count query
|
||||||
response.TotalSize = int32(len(response.Attachments))
|
response.TotalSize = int32(len(response.Attachments))
|
||||||
|
|
||||||
// Set next page token if we got the full page size (indicating there might be more)
|
// 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)
|
response.NextPageToken = fmt.Sprintf("%d", offset+pageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,14 +199,14 @@ func (s *APIV1Service) GetAttachment(ctx context.Context, request *v1pb.GetAttac
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invalid attachment id: %v", err)
|
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 {
|
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 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) {
|
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 {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invalid attachment id: %v", err)
|
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,
|
GetBlob: true,
|
||||||
UID: &attachmentUID,
|
UID: &attachmentUID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "failed to get resource: %v", err)
|
return nil, status.Errorf(codes.Internal, "failed to get attachment: %v", err)
|
||||||
}
|
}
|
||||||
if resource == nil {
|
if attachment == nil {
|
||||||
return nil, status.Errorf(codes.NotFound, "attachment not found")
|
return nil, status.Errorf(codes.NotFound, "attachment not found")
|
||||||
}
|
}
|
||||||
// Check the related memo visibility.
|
// Check the related memo visibility.
|
||||||
if resource.MemoID != nil {
|
if attachment.MemoID != nil {
|
||||||
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
|
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
|
||||||
ID: resource.MemoID,
|
ID: attachment.MemoID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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 {
|
if memo != nil && memo.Visibility != store.Public {
|
||||||
user, err := s.GetCurrentUser(ctx)
|
user, err := s.GetCurrentUser(ctx)
|
||||||
|
|
@ -240,32 +240,32 @@ func (s *APIV1Service) GetAttachmentBinary(ctx context.Context, request *v1pb.Ge
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return nil, status.Errorf(codes.Unauthenticated, "unauthorized access")
|
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")
|
return nil, status.Errorf(codes.Unauthenticated, "unauthorized access")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.Thumbnail && util.HasPrefixes(resource.Type, SupportedThumbnailMimeTypes...) {
|
if request.Thumbnail && util.HasPrefixes(attachment.Type, SupportedThumbnailMimeTypes...) {
|
||||||
thumbnailBlob, err := s.getOrGenerateThumbnail(resource)
|
thumbnailBlob, err := s.getOrGenerateThumbnail(attachment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// thumbnail failures are logged as warnings and not cosidered critical failures as
|
// thumbnail failures are logged as warnings and not cosidered critical failures as
|
||||||
// a resource image can be used in its place.
|
// a attachment image can be used in its place.
|
||||||
slog.Warn("failed to get resource thumbnail image", slog.Any("error", err))
|
slog.Warn("failed to get attachment thumbnail image", slog.Any("error", err))
|
||||||
} else {
|
} else {
|
||||||
return &httpbody.HttpBody{
|
return &httpbody.HttpBody{
|
||||||
ContentType: resource.Type,
|
ContentType: attachment.Type,
|
||||||
Data: thumbnailBlob,
|
Data: thumbnailBlob,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
blob, err := s.GetResourceBlob(resource)
|
blob, err := s.GetAttachmentBlob(attachment)
|
||||||
if err != nil {
|
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/") {
|
if strings.HasPrefix(contentType, "text/") {
|
||||||
contentType += "; charset=utf-8"
|
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 {
|
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "update mask is required")
|
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 {
|
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()
|
currentTs := time.Now().Unix()
|
||||||
update := &store.UpdateResource{
|
update := &store.UpdateAttachment{
|
||||||
ID: resource.ID,
|
ID: attachment.ID,
|
||||||
UpdatedTs: ¤tTs,
|
UpdatedTs: ¤tTs,
|
||||||
}
|
}
|
||||||
for _, field := range request.UpdateMask.Paths {
|
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 {
|
if err := s.Store.UpdateAttachment(ctx, update); 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)
|
||||||
}
|
}
|
||||||
return s.GetAttachment(ctx, &v1pb.GetAttachmentRequest{
|
return s.GetAttachment(ctx, &v1pb.GetAttachmentRequest{
|
||||||
Name: request.Attachment.Name,
|
Name: request.Attachment.Name,
|
||||||
|
|
@ -323,39 +323,39 @@ func (s *APIV1Service) DeleteAttachment(ctx context.Context, request *v1pb.Delet
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
|
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,
|
UID: &attachmentUID,
|
||||||
CreatorID: &user.ID,
|
CreatorID: &user.ID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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")
|
return nil, status.Errorf(codes.NotFound, "attachment not found")
|
||||||
}
|
}
|
||||||
// Delete the resource from the database.
|
// Delete the attachment from the database.
|
||||||
if err := s.Store.DeleteResource(ctx, &store.DeleteResource{
|
if err := s.Store.DeleteAttachment(ctx, &store.DeleteAttachment{
|
||||||
ID: resource.ID,
|
ID: attachment.ID,
|
||||||
}); err != nil {
|
}); 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
|
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{
|
attachmentMessage := &v1pb.Attachment{
|
||||||
Name: fmt.Sprintf("%s%s", AttachmentNamePrefix, resource.UID),
|
Name: fmt.Sprintf("%s%s", AttachmentNamePrefix, attachment.UID),
|
||||||
CreateTime: timestamppb.New(time.Unix(resource.CreatedTs, 0)),
|
CreateTime: timestamppb.New(time.Unix(attachment.CreatedTs, 0)),
|
||||||
Filename: resource.Filename,
|
Filename: attachment.Filename,
|
||||||
Type: resource.Type,
|
Type: attachment.Type,
|
||||||
Size: resource.Size,
|
Size: attachment.Size,
|
||||||
}
|
}
|
||||||
if resource.StorageType == storepb.ResourceStorageType_EXTERNAL || resource.StorageType == storepb.ResourceStorageType_S3 {
|
if attachment.StorageType == storepb.AttachmentStorageType_EXTERNAL || attachment.StorageType == storepb.AttachmentStorageType_S3 {
|
||||||
attachmentMessage.ExternalLink = resource.Reference
|
attachmentMessage.ExternalLink = attachment.Reference
|
||||||
}
|
}
|
||||||
if resource.MemoID != nil {
|
if attachment.MemoID != nil {
|
||||||
memo, _ := s.Store.GetMemo(ctx, &store.FindMemo{
|
memo, _ := s.Store.GetMemo(ctx, &store.FindMemo{
|
||||||
ID: resource.MemoID,
|
ID: attachment.MemoID,
|
||||||
})
|
})
|
||||||
if memo != nil {
|
if memo != nil {
|
||||||
memoName := fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID)
|
memoName := fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID)
|
||||||
|
|
@ -366,8 +366,8 @@ func (s *APIV1Service) convertAttachmentFromStore(ctx context.Context, resource
|
||||||
return attachmentMessage
|
return attachmentMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveResourceBlob save the blob of resource based on the storage config.
|
// SaveAttachmentBlob save the blob of attachment based on the storage config.
|
||||||
func SaveResourceBlob(ctx context.Context, profile *profile.Profile, stores *store.Store, create *store.Resource) error {
|
func SaveAttachmentBlob(ctx context.Context, profile *profile.Profile, stores *store.Store, create *store.Attachment) error {
|
||||||
workspaceStorageSetting, err := stores.GetWorkspaceStorageSetting(ctx)
|
workspaceStorageSetting, err := stores.GetWorkspaceStorageSetting(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Failed to find workspace storage setting")
|
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.Reference = internalPath
|
||||||
create.Blob = nil
|
create.Blob = nil
|
||||||
create.StorageType = storepb.ResourceStorageType_LOCAL
|
create.StorageType = storepb.AttachmentStorageType_LOCAL
|
||||||
} else if workspaceStorageSetting.StorageType == storepb.WorkspaceStorageSetting_S3 {
|
} else if workspaceStorageSetting.StorageType == storepb.WorkspaceStorageSetting_S3 {
|
||||||
s3Config := workspaceStorageSetting.S3Config
|
s3Config := workspaceStorageSetting.S3Config
|
||||||
if s3Config == nil {
|
if s3Config == nil {
|
||||||
|
|
@ -434,10 +434,10 @@ func SaveResourceBlob(ctx context.Context, profile *profile.Profile, stores *sto
|
||||||
|
|
||||||
create.Reference = presignURL
|
create.Reference = presignURL
|
||||||
create.Blob = nil
|
create.Blob = nil
|
||||||
create.StorageType = storepb.ResourceStorageType_S3
|
create.StorageType = storepb.AttachmentStorageType_S3
|
||||||
create.Payload = &storepb.ResourcePayload{
|
create.Payload = &storepb.AttachmentPayload{
|
||||||
Payload: &storepb.ResourcePayload_S3Object_{
|
Payload: &storepb.AttachmentPayload_S3Object_{
|
||||||
S3Object: &storepb.ResourcePayload_S3Object{
|
S3Object: &storepb.AttachmentPayload_S3Object{
|
||||||
S3Config: s3Config,
|
S3Config: s3Config,
|
||||||
Key: key,
|
Key: key,
|
||||||
LastPresignedTime: timestamppb.New(time.Now()),
|
LastPresignedTime: timestamppb.New(time.Now()),
|
||||||
|
|
@ -449,15 +449,15 @@ func SaveResourceBlob(ctx context.Context, profile *profile.Profile, stores *sto
|
||||||
return nil
|
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.
|
// For local storage, read the file from the local disk.
|
||||||
if resource.StorageType == storepb.ResourceStorageType_LOCAL {
|
if attachment.StorageType == storepb.AttachmentStorageType_LOCAL {
|
||||||
resourcePath := filepath.FromSlash(resource.Reference)
|
attachmentPath := filepath.FromSlash(attachment.Reference)
|
||||||
if !filepath.IsAbs(resourcePath) {
|
if !filepath.IsAbs(attachmentPath) {
|
||||||
resourcePath = filepath.Join(s.Profile.Data, resourcePath)
|
attachmentPath = filepath.Join(s.Profile.Data, attachmentPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Open(resourcePath)
|
file, err := os.Open(attachmentPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return nil, errors.Wrap(err, "file not found")
|
return nil, errors.Wrap(err, "file not found")
|
||||||
|
|
@ -472,7 +472,7 @@ func (s *APIV1Service) GetResourceBlob(resource *store.Resource) ([]byte, error)
|
||||||
return blob, nil
|
return blob, nil
|
||||||
}
|
}
|
||||||
// For database storage, return the blob from the database.
|
// For database storage, return the blob from the database.
|
||||||
return resource.Blob, nil
|
return attachment.Blob, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -480,22 +480,22 @@ const (
|
||||||
thumbnailRatio = 0.8
|
thumbnailRatio = 0.8
|
||||||
)
|
)
|
||||||
|
|
||||||
// getOrGenerateThumbnail returns the thumbnail image of the resource.
|
// getOrGenerateThumbnail returns the thumbnail image of the attachment.
|
||||||
func (s *APIV1Service) getOrGenerateThumbnail(resource *store.Resource) ([]byte, error) {
|
func (s *APIV1Service) getOrGenerateThumbnail(attachment *store.Attachment) ([]byte, error) {
|
||||||
thumbnailCacheFolder := filepath.Join(s.Profile.Data, ThumbnailCacheFolder)
|
thumbnailCacheFolder := filepath.Join(s.Profile.Data, ThumbnailCacheFolder)
|
||||||
if err := os.MkdirAll(thumbnailCacheFolder, os.ModePerm); err != nil {
|
if err := os.MkdirAll(thumbnailCacheFolder, os.ModePerm); err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to create thumbnail cache folder")
|
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 _, err := os.Stat(filePath); err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
return nil, errors.Wrap(err, "failed to check thumbnail image stat")
|
return nil, errors.Wrap(err, "failed to check thumbnail image stat")
|
||||||
}
|
}
|
||||||
|
|
||||||
// If thumbnail image does not exist, generate and save the thumbnail image.
|
// 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 {
|
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))
|
img, err := imaging.Decode(bytes.NewReader(blob), imaging.AutoOrientation(true))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -22,54 +22,54 @@ func (s *APIV1Service) SetMemoAttachments(ctx context.Context, request *v1pb.Set
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "failed to get memo")
|
return nil, status.Errorf(codes.Internal, "failed to get memo")
|
||||||
}
|
}
|
||||||
resources, err := s.Store.ListResources(ctx, &store.FindResource{
|
attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{
|
||||||
MemoID: &memo.ID,
|
MemoID: &memo.ID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "failed to list resources")
|
return nil, status.Errorf(codes.Internal, "failed to list attachments")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete resources that are not in the request.
|
// Delete attachments that are not in the request.
|
||||||
for _, resource := range resources {
|
for _, attachment := range attachments {
|
||||||
found := false
|
found := false
|
||||||
for _, requestResource := range request.Attachments {
|
for _, requestAttachment := range request.Attachments {
|
||||||
requestResourceUID, err := ExtractAttachmentUIDFromName(requestResource.Name)
|
requestAttachmentUID, err := ExtractAttachmentUIDFromName(requestAttachment.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invalid attachment name: %v", err)
|
return nil, status.Errorf(codes.InvalidArgument, "invalid attachment name: %v", err)
|
||||||
}
|
}
|
||||||
if resource.UID == requestResourceUID {
|
if attachment.UID == requestAttachmentUID {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
if err = s.Store.DeleteResource(ctx, &store.DeleteResource{
|
if err = s.Store.DeleteAttachment(ctx, &store.DeleteAttachment{
|
||||||
ID: int32(resource.ID),
|
ID: int32(attachment.ID),
|
||||||
MemoID: &memo.ID,
|
MemoID: &memo.ID,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "failed to delete resource")
|
return nil, status.Errorf(codes.Internal, "failed to delete attachment")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
slices.Reverse(request.Attachments)
|
slices.Reverse(request.Attachments)
|
||||||
// Update resources' memo_id in the request.
|
// Update attachments' memo_id in the request.
|
||||||
for index, resource := range request.Attachments {
|
for index, attachment := range request.Attachments {
|
||||||
resourceUID, err := ExtractAttachmentUIDFromName(resource.Name)
|
attachmentUID, err := ExtractAttachmentUIDFromName(attachment.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invalid attachment name: %v", err)
|
return nil, status.Errorf(codes.InvalidArgument, "invalid attachment name: %v", err)
|
||||||
}
|
}
|
||||||
tempResource, err := s.Store.GetResource(ctx, &store.FindResource{UID: &resourceUID})
|
tempAttachment, err := s.Store.GetAttachment(ctx, &store.FindAttachment{UID: &attachmentUID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "failed to get resource: %v", err)
|
return nil, status.Errorf(codes.Internal, "failed to get attachment: %v", err)
|
||||||
}
|
}
|
||||||
updatedTs := time.Now().Unix() + int64(index)
|
updatedTs := time.Now().Unix() + int64(index)
|
||||||
if err := s.Store.UpdateResource(ctx, &store.UpdateResource{
|
if err := s.Store.UpdateAttachment(ctx, &store.UpdateAttachment{
|
||||||
ID: tempResource.ID,
|
ID: tempAttachment.ID,
|
||||||
MemoID: &memo.ID,
|
MemoID: &memo.ID,
|
||||||
UpdatedTs: &updatedTs,
|
UpdatedTs: &updatedTs,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "failed to update resource: %v", err)
|
return nil, status.Errorf(codes.Internal, "failed to update attachment: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,18 +85,18 @@ func (s *APIV1Service) ListMemoAttachments(ctx context.Context, request *v1pb.Li
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "failed to get memo: %v", err)
|
return nil, status.Errorf(codes.Internal, "failed to get memo: %v", err)
|
||||||
}
|
}
|
||||||
resources, err := s.Store.ListResources(ctx, &store.FindResource{
|
attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{
|
||||||
MemoID: &memo.ID,
|
MemoID: &memo.ID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "failed to list resources: %v", err)
|
return nil, status.Errorf(codes.Internal, "failed to list attachments: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
response := &v1pb.ListMemoAttachmentsResponse{
|
response := &v1pb.ListMemoAttachmentsResponse{
|
||||||
Attachments: []*v1pb.Attachment{},
|
Attachments: []*v1pb.Attachment{},
|
||||||
}
|
}
|
||||||
for _, resource := range resources {
|
for _, attachment := range attachments {
|
||||||
response.Attachments = append(response.Attachments, s.convertAttachmentFromStore(ctx, resource))
|
response.Attachments = append(response.Attachments, s.convertAttachmentFromStore(ctx, attachment))
|
||||||
}
|
}
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -399,14 +399,14 @@ func (s *APIV1Service) DeleteMemo(ctx context.Context, request *v1pb.DeleteMemoR
|
||||||
return nil, status.Errorf(codes.Internal, "failed to delete memo relations")
|
return nil, status.Errorf(codes.Internal, "failed to delete memo relations")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete related resources.
|
// Delete related attachments.
|
||||||
resources, err := s.Store.ListResources(ctx, &store.FindResource{MemoID: &memo.ID})
|
attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{MemoID: &memo.ID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "failed to list resources")
|
return nil, status.Errorf(codes.Internal, "failed to list attachments")
|
||||||
}
|
}
|
||||||
for _, resource := range resources {
|
for _, attachment := range attachments {
|
||||||
if err := s.Store.DeleteResource(ctx, &store.DeleteResource{ID: resource.ID}); err != nil {
|
if err := s.Store.DeleteAttachment(ctx, &store.DeleteAttachment{ID: attachment.ID}); err != nil {
|
||||||
return nil, status.Errorf(codes.Internal, "failed to delete resource")
|
return nil, status.Errorf(codes.Internal, "failed to delete attachment")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,22 +124,22 @@ func (s *RSSService) generateRSSFromMemoList(ctx context.Context, memoList []*st
|
||||||
Created: time.Unix(memo.CreatedTs, 0),
|
Created: time.Unix(memo.CreatedTs, 0),
|
||||||
Id: link.Href,
|
Id: link.Href,
|
||||||
}
|
}
|
||||||
resources, err := s.Store.ListResources(ctx, &store.FindResource{
|
attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{
|
||||||
MemoID: &memo.ID,
|
MemoID: &memo.ID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if len(resources) > 0 {
|
if len(attachments) > 0 {
|
||||||
resource := resources[0]
|
attachment := attachments[0]
|
||||||
enclosure := feeds.Enclosure{}
|
enclosure := feeds.Enclosure{}
|
||||||
if resource.StorageType == storepb.ResourceStorageType_EXTERNAL || resource.StorageType == storepb.ResourceStorageType_S3 {
|
if attachment.StorageType == storepb.AttachmentStorageType_EXTERNAL || attachment.StorageType == storepb.AttachmentStorageType_S3 {
|
||||||
enclosure.Url = resource.Reference
|
enclosure.Url = attachment.Reference
|
||||||
} else {
|
} else {
|
||||||
enclosure.Url = fmt.Sprintf("%s/file/attachments/%s/%s", baseURL, resource.UID, resource.Filename)
|
enclosure.Url = fmt.Sprintf("%s/file/attachments/%s/%s", baseURL, attachment.UID, attachment.Filename)
|
||||||
}
|
}
|
||||||
enclosure.Length = strconv.Itoa(int(resource.Size))
|
enclosure.Length = strconv.Itoa(int(attachment.Size))
|
||||||
enclosure.Type = resource.Type
|
enclosure.Type = attachment.Type
|
||||||
feed.Items[i].Enclosure = &enclosure
|
feed.Items[i].Enclosure = &enclosure
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,33 +49,33 @@ func (r *Runner) CheckAndPresign(ctx context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s3StorageType := storepb.ResourceStorageType_S3
|
s3StorageType := storepb.AttachmentStorageType_S3
|
||||||
// Limit resources to a reasonable batch size
|
// Limit attachments to a reasonable batch size
|
||||||
const batchSize = 100
|
const batchSize = 100
|
||||||
offset := 0
|
offset := 0
|
||||||
|
|
||||||
for {
|
for {
|
||||||
limit := batchSize
|
limit := batchSize
|
||||||
resources, err := r.Store.ListResources(ctx, &store.FindResource{
|
attachments, err := r.Store.ListAttachments(ctx, &store.FindAttachment{
|
||||||
GetBlob: false,
|
GetBlob: false,
|
||||||
StorageType: &s3StorageType,
|
StorageType: &s3StorageType,
|
||||||
Limit: &limit,
|
Limit: &limit,
|
||||||
Offset: &offset,
|
Offset: &offset,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to list resources for presigning", "error", err)
|
slog.Error("Failed to list attachments for presigning", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Break if no more resources
|
// Break if no more attachments
|
||||||
if len(resources) == 0 {
|
if len(attachments) == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process batch of resources
|
// Process batch of attachments
|
||||||
presignCount := 0
|
presignCount := 0
|
||||||
for _, resource := range resources {
|
for _, attachment := range attachments {
|
||||||
s3ObjectPayload := resource.Payload.GetS3Object()
|
s3ObjectPayload := attachment.Payload.GetS3Object()
|
||||||
if s3ObjectPayload == nil {
|
if s3ObjectPayload == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -105,30 +105,30 @@ func (r *Runner) CheckAndPresign(ctx context.Context) {
|
||||||
|
|
||||||
presignURL, err := s3Client.PresignGetObject(ctx, s3ObjectPayload.Key)
|
presignURL, err := s3Client.PresignGetObject(ctx, s3ObjectPayload.Key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to presign URL", "error", err, "resourceID", resource.ID)
|
slog.Error("Failed to presign URL", "error", err, "attachmentID", attachment.ID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
s3ObjectPayload.S3Config = s3Config
|
s3ObjectPayload.S3Config = s3Config
|
||||||
s3ObjectPayload.LastPresignedTime = timestamppb.New(time.Now())
|
s3ObjectPayload.LastPresignedTime = timestamppb.New(time.Now())
|
||||||
if err := r.Store.UpdateResource(ctx, &store.UpdateResource{
|
if err := r.Store.UpdateAttachment(ctx, &store.UpdateAttachment{
|
||||||
ID: resource.ID,
|
ID: attachment.ID,
|
||||||
Reference: &presignURL,
|
Reference: &presignURL,
|
||||||
Payload: &storepb.ResourcePayload{
|
Payload: &storepb.AttachmentPayload{
|
||||||
Payload: &storepb.ResourcePayload_S3Object_{
|
Payload: &storepb.AttachmentPayload_S3Object_{
|
||||||
S3Object: s3ObjectPayload,
|
S3Object: s3ObjectPayload,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
slog.Error("Failed to update resource", "error", err, "resourceID", resource.ID)
|
slog.Error("Failed to update attachment", "error", err, "attachmentID", attachment.ID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
presignCount++
|
presignCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("Presigned batch of S3 resources", "batchSize", len(resources), "presigned", presignCount)
|
slog.Info("Presigned batch of S3 attachments", "batchSize", len(attachments), "presigned", presignCount)
|
||||||
|
|
||||||
// Move to next batch
|
// Move to next batch
|
||||||
offset += len(resources)
|
offset += len(attachments)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,10 @@ import (
|
||||||
storepb "github.com/usememos/memos/proto/gen/store"
|
storepb "github.com/usememos/memos/proto/gen/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Resource struct {
|
type Attachment struct {
|
||||||
// ID is the system generated unique identifier for the resource.
|
// ID is the system generated unique identifier for the attachment.
|
||||||
ID int32
|
ID int32
|
||||||
// UID is the user defined unique identifier for the resource.
|
// UID is the user defined unique identifier for the attachment.
|
||||||
UID string
|
UID string
|
||||||
|
|
||||||
// Standard fields
|
// Standard fields
|
||||||
|
|
@ -29,15 +29,15 @@ type Resource struct {
|
||||||
Blob []byte
|
Blob []byte
|
||||||
Type string
|
Type string
|
||||||
Size int64
|
Size int64
|
||||||
StorageType storepb.ResourceStorageType
|
StorageType storepb.AttachmentStorageType
|
||||||
Reference string
|
Reference string
|
||||||
Payload *storepb.ResourcePayload
|
Payload *storepb.AttachmentPayload
|
||||||
|
|
||||||
// The related memo ID.
|
// The related memo ID.
|
||||||
MemoID *int32
|
MemoID *int32
|
||||||
}
|
}
|
||||||
|
|
||||||
type FindResource struct {
|
type FindAttachment struct {
|
||||||
GetBlob bool
|
GetBlob bool
|
||||||
ID *int32
|
ID *int32
|
||||||
UID *string
|
UID *string
|
||||||
|
|
@ -46,35 +46,35 @@ type FindResource struct {
|
||||||
FilenameSearch *string
|
FilenameSearch *string
|
||||||
MemoID *int32
|
MemoID *int32
|
||||||
HasRelatedMemo bool
|
HasRelatedMemo bool
|
||||||
StorageType *storepb.ResourceStorageType
|
StorageType *storepb.AttachmentStorageType
|
||||||
Limit *int
|
Limit *int
|
||||||
Offset *int
|
Offset *int
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateResource struct {
|
type UpdateAttachment struct {
|
||||||
ID int32
|
ID int32
|
||||||
UID *string
|
UID *string
|
||||||
UpdatedTs *int64
|
UpdatedTs *int64
|
||||||
Filename *string
|
Filename *string
|
||||||
MemoID *int32
|
MemoID *int32
|
||||||
Reference *string
|
Reference *string
|
||||||
Payload *storepb.ResourcePayload
|
Payload *storepb.AttachmentPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
type DeleteResource struct {
|
type DeleteAttachment struct {
|
||||||
ID int32
|
ID int32
|
||||||
MemoID *int32
|
MemoID *int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) CreateResource(ctx context.Context, create *Resource) (*Resource, error) {
|
func (s *Store) CreateAttachment(ctx context.Context, create *Attachment) (*Attachment, error) {
|
||||||
if !base.UIDMatcher.MatchString(create.UID) {
|
if !base.UIDMatcher.MatchString(create.UID) {
|
||||||
return nil, errors.New("invalid uid")
|
return nil, errors.New("invalid uid")
|
||||||
}
|
}
|
||||||
return s.driver.CreateResource(ctx, create)
|
return s.driver.CreateAttachment(ctx, create)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) ListResources(ctx context.Context, find *FindResource) ([]*Resource, error) {
|
func (s *Store) ListAttachments(ctx context.Context, find *FindAttachment) ([]*Attachment, error) {
|
||||||
// Set default limits to prevent loading too many resources at once
|
// Set default limits to prevent loading too many attachments at once
|
||||||
if find.Limit == nil && find.GetBlob {
|
if find.Limit == nil && find.GetBlob {
|
||||||
// When fetching blobs, we should be especially careful with limits
|
// When fetching blobs, we should be especially careful with limits
|
||||||
defaultLimit := 10
|
defaultLimit := 10
|
||||||
|
|
@ -85,41 +85,41 @@ func (s *Store) ListResources(ctx context.Context, find *FindResource) ([]*Resou
|
||||||
find.Limit = &defaultLimit
|
find.Limit = &defaultLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.driver.ListResources(ctx, find)
|
return s.driver.ListAttachments(ctx, find)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetResource(ctx context.Context, find *FindResource) (*Resource, error) {
|
func (s *Store) GetAttachment(ctx context.Context, find *FindAttachment) (*Attachment, error) {
|
||||||
resources, err := s.ListResources(ctx, find)
|
attachments, err := s.ListAttachments(ctx, find)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(resources) == 0 {
|
if len(attachments) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return resources[0], nil
|
return attachments[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) UpdateResource(ctx context.Context, update *UpdateResource) error {
|
func (s *Store) UpdateAttachment(ctx context.Context, update *UpdateAttachment) error {
|
||||||
if update.UID != nil && !base.UIDMatcher.MatchString(*update.UID) {
|
if update.UID != nil && !base.UIDMatcher.MatchString(*update.UID) {
|
||||||
return errors.New("invalid uid")
|
return errors.New("invalid uid")
|
||||||
}
|
}
|
||||||
return s.driver.UpdateResource(ctx, update)
|
return s.driver.UpdateAttachment(ctx, update)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) DeleteResource(ctx context.Context, delete *DeleteResource) error {
|
func (s *Store) DeleteAttachment(ctx context.Context, delete *DeleteAttachment) error {
|
||||||
resource, err := s.GetResource(ctx, &FindResource{ID: &delete.ID})
|
attachment, err := s.GetAttachment(ctx, &FindAttachment{ID: &delete.ID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to get resource")
|
return errors.Wrap(err, "failed to get attachment")
|
||||||
}
|
}
|
||||||
if resource == nil {
|
if attachment == nil {
|
||||||
return errors.New("resource not found")
|
return errors.New("attachment not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
if resource.StorageType == storepb.ResourceStorageType_LOCAL {
|
if attachment.StorageType == storepb.AttachmentStorageType_LOCAL {
|
||||||
if err := func() error {
|
if err := func() error {
|
||||||
p := filepath.FromSlash(resource.Reference)
|
p := filepath.FromSlash(attachment.Reference)
|
||||||
if !filepath.IsAbs(p) {
|
if !filepath.IsAbs(p) {
|
||||||
p = filepath.Join(s.profile.Data, p)
|
p = filepath.Join(s.profile.Data, p)
|
||||||
}
|
}
|
||||||
|
|
@ -131,9 +131,9 @@ func (s *Store) DeleteResource(ctx context.Context, delete *DeleteResource) erro
|
||||||
}(); err != nil {
|
}(); err != nil {
|
||||||
return errors.Wrap(err, "failed to delete local file")
|
return errors.Wrap(err, "failed to delete local file")
|
||||||
}
|
}
|
||||||
} else if resource.StorageType == storepb.ResourceStorageType_S3 {
|
} else if attachment.StorageType == storepb.AttachmentStorageType_S3 {
|
||||||
if err := func() error {
|
if err := func() error {
|
||||||
s3ObjectPayload := resource.Payload.GetS3Object()
|
s3ObjectPayload := attachment.Payload.GetS3Object()
|
||||||
if s3ObjectPayload == nil {
|
if s3ObjectPayload == nil {
|
||||||
return errors.Errorf("No s3 object found")
|
return errors.Errorf("No s3 object found")
|
||||||
}
|
}
|
||||||
|
|
@ -162,5 +162,5 @@ func (s *Store) DeleteResource(ctx context.Context, delete *DeleteResource) erro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.driver.DeleteResource(ctx, delete)
|
return s.driver.DeleteAttachment(ctx, delete)
|
||||||
}
|
}
|
||||||
|
|
@ -13,18 +13,18 @@ import (
|
||||||
"github.com/usememos/memos/store"
|
"github.com/usememos/memos/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store.Resource, error) {
|
func (d *DB) CreateAttachment(ctx context.Context, create *store.Attachment) (*store.Attachment, error) {
|
||||||
fields := []string{"`uid`", "`filename`", "`blob`", "`type`", "`size`", "`creator_id`", "`memo_id`", "`storage_type`", "`reference`", "`payload`"}
|
fields := []string{"`uid`", "`filename`", "`blob`", "`type`", "`size`", "`creator_id`", "`memo_id`", "`storage_type`", "`reference`", "`payload`"}
|
||||||
placeholder := []string{"?", "?", "?", "?", "?", "?", "?", "?", "?", "?"}
|
placeholder := []string{"?", "?", "?", "?", "?", "?", "?", "?", "?", "?"}
|
||||||
storageType := ""
|
storageType := ""
|
||||||
if create.StorageType != storepb.ResourceStorageType_RESOURCE_STORAGE_TYPE_UNSPECIFIED {
|
if create.StorageType != storepb.AttachmentStorageType_ATTACHMENT_STORAGE_TYPE_UNSPECIFIED {
|
||||||
storageType = create.StorageType.String()
|
storageType = create.StorageType.String()
|
||||||
}
|
}
|
||||||
payloadString := "{}"
|
payloadString := "{}"
|
||||||
if create.Payload != nil {
|
if create.Payload != nil {
|
||||||
bytes, err := protojson.Marshal(create.Payload)
|
bytes, err := protojson.Marshal(create.Payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to marshal resource payload")
|
return nil, errors.Wrap(err, "failed to marshal attachment payload")
|
||||||
}
|
}
|
||||||
payloadString = string(bytes)
|
payloadString = string(bytes)
|
||||||
}
|
}
|
||||||
|
|
@ -42,10 +42,10 @@ func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store
|
||||||
}
|
}
|
||||||
|
|
||||||
id32 := int32(id)
|
id32 := int32(id)
|
||||||
return d.GetResource(ctx, &store.FindResource{ID: &id32})
|
return d.GetAttachment(ctx, &store.FindAttachment{ID: &id32})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*store.Resource, error) {
|
func (d *DB) ListAttachments(ctx context.Context, find *store.FindAttachment) ([]*store.Attachment, error) {
|
||||||
where, args := []string{"1 = 1"}, []any{}
|
where, args := []string{"1 = 1"}, []any{}
|
||||||
|
|
||||||
if v := find.ID; v != nil {
|
if v := find.ID; v != nil {
|
||||||
|
|
@ -92,43 +92,43 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
list := make([]*store.Resource, 0)
|
list := make([]*store.Attachment, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
resource := store.Resource{}
|
attachment := store.Attachment{}
|
||||||
var memoID sql.NullInt32
|
var memoID sql.NullInt32
|
||||||
var storageType string
|
var storageType string
|
||||||
var payloadBytes []byte
|
var payloadBytes []byte
|
||||||
dests := []any{
|
dests := []any{
|
||||||
&resource.ID,
|
&attachment.ID,
|
||||||
&resource.UID,
|
&attachment.UID,
|
||||||
&resource.Filename,
|
&attachment.Filename,
|
||||||
&resource.Type,
|
&attachment.Type,
|
||||||
&resource.Size,
|
&attachment.Size,
|
||||||
&resource.CreatorID,
|
&attachment.CreatorID,
|
||||||
&resource.CreatedTs,
|
&attachment.CreatedTs,
|
||||||
&resource.UpdatedTs,
|
&attachment.UpdatedTs,
|
||||||
&memoID,
|
&memoID,
|
||||||
&storageType,
|
&storageType,
|
||||||
&resource.Reference,
|
&attachment.Reference,
|
||||||
&payloadBytes,
|
&payloadBytes,
|
||||||
}
|
}
|
||||||
if find.GetBlob {
|
if find.GetBlob {
|
||||||
dests = append(dests, &resource.Blob)
|
dests = append(dests, &attachment.Blob)
|
||||||
}
|
}
|
||||||
if err := rows.Scan(dests...); err != nil {
|
if err := rows.Scan(dests...); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if memoID.Valid {
|
if memoID.Valid {
|
||||||
resource.MemoID = &memoID.Int32
|
attachment.MemoID = &memoID.Int32
|
||||||
}
|
}
|
||||||
resource.StorageType = storepb.ResourceStorageType(storepb.ResourceStorageType_value[storageType])
|
attachment.StorageType = storepb.AttachmentStorageType(storepb.AttachmentStorageType_value[storageType])
|
||||||
payload := &storepb.ResourcePayload{}
|
payload := &storepb.AttachmentPayload{}
|
||||||
if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {
|
if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
resource.Payload = payload
|
attachment.Payload = payload
|
||||||
list = append(list, &resource)
|
list = append(list, &attachment)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
|
|
@ -138,8 +138,8 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
|
||||||
return list, nil
|
return list, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DB) GetResource(ctx context.Context, find *store.FindResource) (*store.Resource, error) {
|
func (d *DB) GetAttachment(ctx context.Context, find *store.FindAttachment) (*store.Attachment, error) {
|
||||||
list, err := d.ListResources(ctx, find)
|
list, err := d.ListAttachments(ctx, find)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -150,7 +150,7 @@ func (d *DB) GetResource(ctx context.Context, find *store.FindResource) (*store.
|
||||||
return list[0], nil
|
return list[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) error {
|
func (d *DB) UpdateAttachment(ctx context.Context, update *store.UpdateAttachment) error {
|
||||||
set, args := []string{}, []any{}
|
set, args := []string{}, []any{}
|
||||||
|
|
||||||
if v := update.UID; v != nil {
|
if v := update.UID; v != nil {
|
||||||
|
|
@ -171,7 +171,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
|
||||||
if v := update.Payload; v != nil {
|
if v := update.Payload; v != nil {
|
||||||
bytes, err := protojson.Marshal(v)
|
bytes, err := protojson.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to marshal resource payload")
|
return errors.Wrap(err, "failed to marshal attachment payload")
|
||||||
}
|
}
|
||||||
set, args = append(set, "`payload` = ?"), append(args, string(bytes))
|
set, args = append(set, "`payload` = ?"), append(args, string(bytes))
|
||||||
}
|
}
|
||||||
|
|
@ -188,7 +188,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DB) DeleteResource(ctx context.Context, delete *store.DeleteResource) error {
|
func (d *DB) DeleteAttachment(ctx context.Context, delete *store.DeleteAttachment) error {
|
||||||
stmt := "DELETE FROM `resource` WHERE `id` = ?"
|
stmt := "DELETE FROM `resource` WHERE `id` = ?"
|
||||||
result, err := d.db.ExecContext(ctx, stmt, delete.ID)
|
result, err := d.db.ExecContext(ctx, stmt, delete.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -13,17 +13,17 @@ import (
|
||||||
"github.com/usememos/memos/store"
|
"github.com/usememos/memos/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store.Resource, error) {
|
func (d *DB) CreateAttachment(ctx context.Context, create *store.Attachment) (*store.Attachment, error) {
|
||||||
fields := []string{"uid", "filename", "blob", "type", "size", "creator_id", "memo_id", "storage_type", "reference", "payload"}
|
fields := []string{"uid", "filename", "blob", "type", "size", "creator_id", "memo_id", "storage_type", "reference", "payload"}
|
||||||
storageType := ""
|
storageType := ""
|
||||||
if create.StorageType != storepb.ResourceStorageType_RESOURCE_STORAGE_TYPE_UNSPECIFIED {
|
if create.StorageType != storepb.AttachmentStorageType_ATTACHMENT_STORAGE_TYPE_UNSPECIFIED {
|
||||||
storageType = create.StorageType.String()
|
storageType = create.StorageType.String()
|
||||||
}
|
}
|
||||||
payloadString := "{}"
|
payloadString := "{}"
|
||||||
if create.Payload != nil {
|
if create.Payload != nil {
|
||||||
bytes, err := protojson.Marshal(create.Payload)
|
bytes, err := protojson.Marshal(create.Payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to marshal resource payload")
|
return nil, errors.Wrap(err, "failed to marshal attachment payload")
|
||||||
}
|
}
|
||||||
payloadString = string(bytes)
|
payloadString = string(bytes)
|
||||||
}
|
}
|
||||||
|
|
@ -36,7 +36,7 @@ func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store
|
||||||
return create, nil
|
return create, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*store.Resource, error) {
|
func (d *DB) ListAttachments(ctx context.Context, find *store.FindAttachment) ([]*store.Attachment, error) {
|
||||||
where, args := []string{"1 = 1"}, []any{}
|
where, args := []string{"1 = 1"}, []any{}
|
||||||
|
|
||||||
if v := find.ID; v != nil {
|
if v := find.ID; v != nil {
|
||||||
|
|
@ -89,43 +89,43 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
list := make([]*store.Resource, 0)
|
list := make([]*store.Attachment, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
resource := store.Resource{}
|
attachment := store.Attachment{}
|
||||||
var memoID sql.NullInt32
|
var memoID sql.NullInt32
|
||||||
var storageType string
|
var storageType string
|
||||||
var payloadBytes []byte
|
var payloadBytes []byte
|
||||||
dests := []any{
|
dests := []any{
|
||||||
&resource.ID,
|
&attachment.ID,
|
||||||
&resource.UID,
|
&attachment.UID,
|
||||||
&resource.Filename,
|
&attachment.Filename,
|
||||||
&resource.Type,
|
&attachment.Type,
|
||||||
&resource.Size,
|
&attachment.Size,
|
||||||
&resource.CreatorID,
|
&attachment.CreatorID,
|
||||||
&resource.CreatedTs,
|
&attachment.CreatedTs,
|
||||||
&resource.UpdatedTs,
|
&attachment.UpdatedTs,
|
||||||
&memoID,
|
&memoID,
|
||||||
&storageType,
|
&storageType,
|
||||||
&resource.Reference,
|
&attachment.Reference,
|
||||||
&payloadBytes,
|
&payloadBytes,
|
||||||
}
|
}
|
||||||
if find.GetBlob {
|
if find.GetBlob {
|
||||||
dests = append(dests, &resource.Blob)
|
dests = append(dests, &attachment.Blob)
|
||||||
}
|
}
|
||||||
if err := rows.Scan(dests...); err != nil {
|
if err := rows.Scan(dests...); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if memoID.Valid {
|
if memoID.Valid {
|
||||||
resource.MemoID = &memoID.Int32
|
attachment.MemoID = &memoID.Int32
|
||||||
}
|
}
|
||||||
resource.StorageType = storepb.ResourceStorageType(storepb.ResourceStorageType_value[storageType])
|
attachment.StorageType = storepb.AttachmentStorageType(storepb.AttachmentStorageType_value[storageType])
|
||||||
payload := &storepb.ResourcePayload{}
|
payload := &storepb.AttachmentPayload{}
|
||||||
if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {
|
if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
resource.Payload = payload
|
attachment.Payload = payload
|
||||||
list = append(list, &resource)
|
list = append(list, &attachment)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
|
|
@ -135,7 +135,7 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
|
||||||
return list, nil
|
return list, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) error {
|
func (d *DB) UpdateAttachment(ctx context.Context, update *store.UpdateAttachment) error {
|
||||||
set, args := []string{}, []any{}
|
set, args := []string{}, []any{}
|
||||||
|
|
||||||
if v := update.UID; v != nil {
|
if v := update.UID; v != nil {
|
||||||
|
|
@ -156,7 +156,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
|
||||||
if v := update.Payload; v != nil {
|
if v := update.Payload; v != nil {
|
||||||
bytes, err := protojson.Marshal(v)
|
bytes, err := protojson.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to marshal resource payload")
|
return errors.Wrap(err, "failed to marshal attachment payload")
|
||||||
}
|
}
|
||||||
set, args = append(set, "payload = "+placeholder(len(args)+1)), append(args, string(bytes))
|
set, args = append(set, "payload = "+placeholder(len(args)+1)), append(args, string(bytes))
|
||||||
}
|
}
|
||||||
|
|
@ -173,7 +173,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DB) DeleteResource(ctx context.Context, delete *store.DeleteResource) error {
|
func (d *DB) DeleteAttachment(ctx context.Context, delete *store.DeleteAttachment) error {
|
||||||
stmt := `DELETE FROM resource WHERE id = $1`
|
stmt := `DELETE FROM resource WHERE id = $1`
|
||||||
result, err := d.db.ExecContext(ctx, stmt, delete.ID)
|
result, err := d.db.ExecContext(ctx, stmt, delete.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -13,18 +13,18 @@ import (
|
||||||
"github.com/usememos/memos/store"
|
"github.com/usememos/memos/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store.Resource, error) {
|
func (d *DB) CreateAttachment(ctx context.Context, create *store.Attachment) (*store.Attachment, error) {
|
||||||
fields := []string{"`uid`", "`filename`", "`blob`", "`type`", "`size`", "`creator_id`", "`memo_id`", "`storage_type`", "`reference`", "`payload`"}
|
fields := []string{"`uid`", "`filename`", "`blob`", "`type`", "`size`", "`creator_id`", "`memo_id`", "`storage_type`", "`reference`", "`payload`"}
|
||||||
placeholder := []string{"?", "?", "?", "?", "?", "?", "?", "?", "?", "?"}
|
placeholder := []string{"?", "?", "?", "?", "?", "?", "?", "?", "?", "?"}
|
||||||
storageType := ""
|
storageType := ""
|
||||||
if create.StorageType != storepb.ResourceStorageType_RESOURCE_STORAGE_TYPE_UNSPECIFIED {
|
if create.StorageType != storepb.AttachmentStorageType_ATTACHMENT_STORAGE_TYPE_UNSPECIFIED {
|
||||||
storageType = create.StorageType.String()
|
storageType = create.StorageType.String()
|
||||||
}
|
}
|
||||||
payloadString := "{}"
|
payloadString := "{}"
|
||||||
if create.Payload != nil {
|
if create.Payload != nil {
|
||||||
bytes, err := protojson.Marshal(create.Payload)
|
bytes, err := protojson.Marshal(create.Payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to marshal resource payload")
|
return nil, errors.Wrap(err, "failed to marshal attachment payload")
|
||||||
}
|
}
|
||||||
payloadString = string(bytes)
|
payloadString = string(bytes)
|
||||||
}
|
}
|
||||||
|
|
@ -38,7 +38,7 @@ func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store
|
||||||
return create, nil
|
return create, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*store.Resource, error) {
|
func (d *DB) ListAttachments(ctx context.Context, find *store.FindAttachment) ([]*store.Attachment, error) {
|
||||||
where, args := []string{"1 = 1"}, []any{}
|
where, args := []string{"1 = 1"}, []any{}
|
||||||
|
|
||||||
if v := find.ID; v != nil {
|
if v := find.ID; v != nil {
|
||||||
|
|
@ -85,43 +85,43 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
list := make([]*store.Resource, 0)
|
list := make([]*store.Attachment, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
resource := store.Resource{}
|
attachment := store.Attachment{}
|
||||||
var memoID sql.NullInt32
|
var memoID sql.NullInt32
|
||||||
var storageType string
|
var storageType string
|
||||||
var payloadBytes []byte
|
var payloadBytes []byte
|
||||||
dests := []any{
|
dests := []any{
|
||||||
&resource.ID,
|
&attachment.ID,
|
||||||
&resource.UID,
|
&attachment.UID,
|
||||||
&resource.Filename,
|
&attachment.Filename,
|
||||||
&resource.Type,
|
&attachment.Type,
|
||||||
&resource.Size,
|
&attachment.Size,
|
||||||
&resource.CreatorID,
|
&attachment.CreatorID,
|
||||||
&resource.CreatedTs,
|
&attachment.CreatedTs,
|
||||||
&resource.UpdatedTs,
|
&attachment.UpdatedTs,
|
||||||
&memoID,
|
&memoID,
|
||||||
&storageType,
|
&storageType,
|
||||||
&resource.Reference,
|
&attachment.Reference,
|
||||||
&payloadBytes,
|
&payloadBytes,
|
||||||
}
|
}
|
||||||
if find.GetBlob {
|
if find.GetBlob {
|
||||||
dests = append(dests, &resource.Blob)
|
dests = append(dests, &attachment.Blob)
|
||||||
}
|
}
|
||||||
if err := rows.Scan(dests...); err != nil {
|
if err := rows.Scan(dests...); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if memoID.Valid {
|
if memoID.Valid {
|
||||||
resource.MemoID = &memoID.Int32
|
attachment.MemoID = &memoID.Int32
|
||||||
}
|
}
|
||||||
resource.StorageType = storepb.ResourceStorageType(storepb.ResourceStorageType_value[storageType])
|
attachment.StorageType = storepb.AttachmentStorageType(storepb.AttachmentStorageType_value[storageType])
|
||||||
payload := &storepb.ResourcePayload{}
|
payload := &storepb.AttachmentPayload{}
|
||||||
if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {
|
if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
resource.Payload = payload
|
attachment.Payload = payload
|
||||||
list = append(list, &resource)
|
list = append(list, &attachment)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
|
|
@ -131,7 +131,7 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
|
||||||
return list, nil
|
return list, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) error {
|
func (d *DB) UpdateAttachment(ctx context.Context, update *store.UpdateAttachment) error {
|
||||||
set, args := []string{}, []any{}
|
set, args := []string{}, []any{}
|
||||||
|
|
||||||
if v := update.UID; v != nil {
|
if v := update.UID; v != nil {
|
||||||
|
|
@ -152,7 +152,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
|
||||||
if v := update.Payload; v != nil {
|
if v := update.Payload; v != nil {
|
||||||
bytes, err := protojson.Marshal(v)
|
bytes, err := protojson.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to marshal resource payload")
|
return errors.Wrap(err, "failed to marshal attachment payload")
|
||||||
}
|
}
|
||||||
set, args = append(set, "`payload` = ?"), append(args, string(bytes))
|
set, args = append(set, "`payload` = ?"), append(args, string(bytes))
|
||||||
}
|
}
|
||||||
|
|
@ -161,7 +161,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
|
||||||
stmt := "UPDATE `resource` SET " + strings.Join(set, ", ") + " WHERE `id` = ?"
|
stmt := "UPDATE `resource` SET " + strings.Join(set, ", ") + " WHERE `id` = ?"
|
||||||
result, err := d.db.ExecContext(ctx, stmt, args...)
|
result, err := d.db.ExecContext(ctx, stmt, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to update resource")
|
return errors.Wrap(err, "failed to update attachment")
|
||||||
}
|
}
|
||||||
if _, err := result.RowsAffected(); err != nil {
|
if _, err := result.RowsAffected(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -169,7 +169,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DB) DeleteResource(ctx context.Context, delete *store.DeleteResource) error {
|
func (d *DB) DeleteAttachment(ctx context.Context, delete *store.DeleteAttachment) error {
|
||||||
stmt := "DELETE FROM `resource` WHERE `id` = ?"
|
stmt := "DELETE FROM `resource` WHERE `id` = ?"
|
||||||
result, err := d.db.ExecContext(ctx, stmt, delete.ID)
|
result, err := d.db.ExecContext(ctx, stmt, delete.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -25,11 +25,11 @@ type Driver interface {
|
||||||
CreateActivity(ctx context.Context, create *Activity) (*Activity, error)
|
CreateActivity(ctx context.Context, create *Activity) (*Activity, error)
|
||||||
ListActivities(ctx context.Context, find *FindActivity) ([]*Activity, error)
|
ListActivities(ctx context.Context, find *FindActivity) ([]*Activity, error)
|
||||||
|
|
||||||
// Resource model related methods.
|
// Attachment model related methods.
|
||||||
CreateResource(ctx context.Context, create *Resource) (*Resource, error)
|
CreateAttachment(ctx context.Context, create *Attachment) (*Attachment, error)
|
||||||
ListResources(ctx context.Context, find *FindResource) ([]*Resource, error)
|
ListAttachments(ctx context.Context, find *FindAttachment) ([]*Attachment, error)
|
||||||
UpdateResource(ctx context.Context, update *UpdateResource) error
|
UpdateAttachment(ctx context.Context, update *UpdateAttachment) error
|
||||||
DeleteResource(ctx context.Context, delete *DeleteResource) error
|
DeleteAttachment(ctx context.Context, delete *DeleteAttachment) error
|
||||||
|
|
||||||
// Memo model related methods.
|
// Memo model related methods.
|
||||||
CreateMemo(ctx context.Context, create *Memo) (*Memo, error)
|
CreateMemo(ctx context.Context, create *Memo) (*Memo, error)
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,10 @@ import (
|
||||||
"github.com/usememos/memos/store"
|
"github.com/usememos/memos/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestResourceStore(t *testing.T) {
|
func TestAttachmentStore(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ts := NewTestingStore(ctx, t)
|
ts := NewTestingStore(ctx, t)
|
||||||
_, err := ts.CreateResource(ctx, &store.Resource{
|
_, err := ts.CreateAttachment(ctx, &store.Attachment{
|
||||||
UID: shortuuid.New(),
|
UID: shortuuid.New(),
|
||||||
CreatorID: 101,
|
CreatorID: 101,
|
||||||
Filename: "test.epub",
|
Filename: "test.epub",
|
||||||
|
|
@ -25,39 +25,39 @@ func TestResourceStore(t *testing.T) {
|
||||||
|
|
||||||
correctFilename := "test.epub"
|
correctFilename := "test.epub"
|
||||||
incorrectFilename := "test.png"
|
incorrectFilename := "test.png"
|
||||||
resource, err := ts.GetResource(ctx, &store.FindResource{
|
attachment, err := ts.GetAttachment(ctx, &store.FindAttachment{
|
||||||
Filename: &correctFilename,
|
Filename: &correctFilename,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, correctFilename, resource.Filename)
|
require.Equal(t, correctFilename, attachment.Filename)
|
||||||
require.Equal(t, int32(1), resource.ID)
|
require.Equal(t, int32(1), attachment.ID)
|
||||||
|
|
||||||
notFoundResource, err := ts.GetResource(ctx, &store.FindResource{
|
notFoundAttachment, err := ts.GetAttachment(ctx, &store.FindAttachment{
|
||||||
Filename: &incorrectFilename,
|
Filename: &incorrectFilename,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Nil(t, notFoundResource)
|
require.Nil(t, notFoundAttachment)
|
||||||
|
|
||||||
var correctCreatorID int32 = 101
|
var correctCreatorID int32 = 101
|
||||||
var incorrectCreatorID int32 = 102
|
var incorrectCreatorID int32 = 102
|
||||||
_, err = ts.GetResource(ctx, &store.FindResource{
|
_, err = ts.GetAttachment(ctx, &store.FindAttachment{
|
||||||
CreatorID: &correctCreatorID,
|
CreatorID: &correctCreatorID,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
notFoundResource, err = ts.GetResource(ctx, &store.FindResource{
|
notFoundAttachment, err = ts.GetAttachment(ctx, &store.FindAttachment{
|
||||||
CreatorID: &incorrectCreatorID,
|
CreatorID: &incorrectCreatorID,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Nil(t, notFoundResource)
|
require.Nil(t, notFoundAttachment)
|
||||||
|
|
||||||
err = ts.DeleteResource(ctx, &store.DeleteResource{
|
err = ts.DeleteAttachment(ctx, &store.DeleteAttachment{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = ts.DeleteResource(ctx, &store.DeleteResource{
|
err = ts.DeleteAttachment(ctx, &store.DeleteAttachment{
|
||||||
ID: 2,
|
ID: 2,
|
||||||
})
|
})
|
||||||
require.ErrorContains(t, err, "resource not found")
|
require.ErrorContains(t, err, "attachment not found")
|
||||||
ts.Close()
|
ts.Close()
|
||||||
}
|
}
|
||||||
|
|
@ -17,15 +17,15 @@ import showPreviewImageDialog from "./PreviewImageDialog";
|
||||||
import SquareDiv from "./kit/SquareDiv";
|
import SquareDiv from "./kit/SquareDiv";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
resource: Attachment;
|
attachment: Attachment;
|
||||||
className?: string;
|
className?: string;
|
||||||
strokeWidth?: number;
|
strokeWidth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ResourceIcon = (props: Props) => {
|
const AttachmentIcon = (props: Props) => {
|
||||||
const { resource } = props;
|
const { attachment } = props;
|
||||||
const resourceType = getAttachmentType(resource);
|
const resourceType = getAttachmentType(attachment);
|
||||||
const resourceUrl = getAttachmentUrl(resource);
|
const resourceUrl = getAttachmentUrl(attachment);
|
||||||
const className = cn("w-full h-auto", props.className);
|
const className = cn("w-full h-auto", props.className);
|
||||||
const strokeWidth = props.strokeWidth;
|
const strokeWidth = props.strokeWidth;
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ const ResourceIcon = (props: Props) => {
|
||||||
<SquareDiv className={cn(className, "flex items-center justify-center overflow-clip")}>
|
<SquareDiv className={cn(className, "flex items-center justify-center overflow-clip")}>
|
||||||
<img
|
<img
|
||||||
className="min-w-full min-h-full object-cover"
|
className="min-w-full min-h-full object-cover"
|
||||||
src={resource.externalLink ? resourceUrl : resourceUrl + "?thumbnail=true"}
|
src={attachment.externalLink ? resourceUrl : resourceUrl + "?thumbnail=true"}
|
||||||
onClick={() => showPreviewImageDialog(resourceUrl)}
|
onClick={() => showPreviewImageDialog(resourceUrl)}
|
||||||
decoding="async"
|
decoding="async"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|
@ -47,7 +47,7 @@ const ResourceIcon = (props: Props) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getResourceIcon = () => {
|
const getAttachmentIcon = () => {
|
||||||
switch (resourceType) {
|
switch (resourceType) {
|
||||||
case "video/*":
|
case "video/*":
|
||||||
return <FileVideo2Icon strokeWidth={strokeWidth} className="w-full h-auto" />;
|
return <FileVideo2Icon strokeWidth={strokeWidth} className="w-full h-auto" />;
|
||||||
|
|
@ -74,9 +74,9 @@ const ResourceIcon = (props: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onClick={previewResource} className={cn(className, "max-w-16 opacity-50")}>
|
<div onClick={previewResource} className={cn(className, "max-w-16 opacity-50")}>
|
||||||
{getResourceIcon()}
|
{getAttachmentIcon()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo(ResourceIcon);
|
export default React.memo(AttachmentIcon);
|
||||||
|
|
@ -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 { Attachment } from "@/types/proto/api/v1/attachment_service";
|
||||||
import { cn } from "@/utils";
|
import { cn } from "@/utils";
|
||||||
import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment";
|
import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment";
|
||||||
import MemoResource from "./MemoResource";
|
import MemoAttachment from "./MemoAttachment";
|
||||||
import showPreviewImageDialog from "./PreviewImageDialog";
|
import showPreviewImageDialog from "./PreviewImageDialog";
|
||||||
|
|
||||||
const MemoAttachmentListView = ({ attachments = [] }: { attachments: Attachment[] }) => {
|
const MemoAttachmentListView = ({ attachments = [] }: { attachments: Attachment[] }) => {
|
||||||
|
|
@ -78,7 +78,7 @@ const MemoAttachmentListView = ({ attachments = [] }: { attachments: Attachment[
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-row justify-start overflow-auto gap-2">
|
<div className="w-full flex flex-row justify-start overflow-auto gap-2">
|
||||||
{otherAttachments.map((attachment) => (
|
{otherAttachments.map((attachment) => (
|
||||||
<MemoResource key={attachment.name} resource={attachment} />
|
<MemoAttachment key={attachment.name} attachment={attachment} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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 { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
import { Attachment } from "@/types/proto/api/v1/attachment_service";
|
import { Attachment } from "@/types/proto/api/v1/attachment_service";
|
||||||
import ResourceIcon from "../ResourceIcon";
|
import AttachmentIcon from "../AttachmentIcon";
|
||||||
import SortableItem from "./SortableItem";
|
import SortableItem from "./SortableItem";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -41,7 +41,7 @@ const AttachmentListView = (props: Props) => {
|
||||||
className="max-w-full w-auto flex flex-row justify-start items-center flex-nowrap gap-x-1 bg-zinc-100 dark:bg-zinc-900 px-2 py-1 rounded hover:shadow-sm text-gray-500 dark:text-gray-400"
|
className="max-w-full w-auto flex flex-row justify-start items-center flex-nowrap gap-x-1 bg-zinc-100 dark:bg-zinc-900 px-2 py-1 rounded hover:shadow-sm text-gray-500 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
<SortableItem id={attachment.name} className="flex flex-row justify-start items-center gap-x-1">
|
<SortableItem id={attachment.name} className="flex flex-row justify-start items-center gap-x-1">
|
||||||
<ResourceIcon resource={attachment} className="w-4! h-4! opacity-100!" />
|
<AttachmentIcon attachment={attachment} className="w-4! h-4! opacity-100!" />
|
||||||
<span className="text-sm max-w-32 truncate">{attachment.filename}</span>
|
<span className="text-sm max-w-32 truncate">{attachment.filename}</span>
|
||||||
</SortableItem>
|
</SortableItem>
|
||||||
<button className="shrink-0" onClick={() => handleDeleteAttachment(attachment.name)}>
|
<button className="shrink-0" onClick={() => handleDeleteAttachment(attachment.name)}>
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import AddMemoRelationPopover from "./ActionButton/AddMemoRelationPopover";
|
||||||
import LocationSelector from "./ActionButton/LocationSelector";
|
import LocationSelector from "./ActionButton/LocationSelector";
|
||||||
import MarkdownMenu from "./ActionButton/MarkdownMenu";
|
import MarkdownMenu from "./ActionButton/MarkdownMenu";
|
||||||
import TagSelector from "./ActionButton/TagSelector";
|
import TagSelector from "./ActionButton/TagSelector";
|
||||||
import UploadResourceButton from "./ActionButton/UploadResourceButton";
|
import UploadAttachmentButton from "./ActionButton/UploadAttachmentButton";
|
||||||
import VisibilitySelector from "./ActionButton/VisibilitySelector";
|
import VisibilitySelector from "./ActionButton/VisibilitySelector";
|
||||||
import AttachmentListView from "./AttachmentListView";
|
import AttachmentListView from "./AttachmentListView";
|
||||||
import Editor, { EditorRefActions } from "./Editor";
|
import Editor, { EditorRefActions } from "./Editor";
|
||||||
|
|
@ -51,7 +51,7 @@ interface State {
|
||||||
attachmentList: Attachment[];
|
attachmentList: Attachment[];
|
||||||
relationList: MemoRelation[];
|
relationList: MemoRelation[];
|
||||||
location: Location | undefined;
|
location: Location | undefined;
|
||||||
isUploadingResource: boolean;
|
isUploadingAttachment: boolean;
|
||||||
isRequesting: boolean;
|
isRequesting: boolean;
|
||||||
isComposing: boolean;
|
isComposing: boolean;
|
||||||
isDraggingFile: boolean;
|
isDraggingFile: boolean;
|
||||||
|
|
@ -67,7 +67,7 @@ const MemoEditor = observer((props: Props) => {
|
||||||
attachmentList: [],
|
attachmentList: [],
|
||||||
relationList: [],
|
relationList: [],
|
||||||
location: undefined,
|
location: undefined,
|
||||||
isUploadingResource: false,
|
isUploadingAttachment: false,
|
||||||
isRequesting: false,
|
isRequesting: false,
|
||||||
isComposing: false,
|
isComposing: false,
|
||||||
isDraggingFile: false,
|
isDraggingFile: false,
|
||||||
|
|
@ -203,7 +203,7 @@ const MemoEditor = observer((props: Props) => {
|
||||||
setState((state) => {
|
setState((state) => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isUploadingResource: true,
|
isUploadingAttachment: true,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -223,7 +223,7 @@ const MemoEditor = observer((props: Props) => {
|
||||||
setState((state) => {
|
setState((state) => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isUploadingResource: false,
|
isUploadingAttachment: false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return attachment;
|
return attachment;
|
||||||
|
|
@ -233,7 +233,7 @@ const MemoEditor = observer((props: Props) => {
|
||||||
setState((state) => {
|
setState((state) => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isUploadingResource: false,
|
isUploadingAttachment: false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -456,7 +456,7 @@ const MemoEditor = observer((props: Props) => {
|
||||||
[i18n.language],
|
[i18n.language],
|
||||||
);
|
);
|
||||||
|
|
||||||
const allowSave = (hasContent || state.attachmentList.length > 0) && !state.isUploadingResource && !state.isRequesting;
|
const allowSave = (hasContent || state.attachmentList.length > 0) && !state.isUploadingAttachment && !state.isRequesting;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MemoEditorContext.Provider
|
<MemoEditorContext.Provider
|
||||||
|
|
@ -502,7 +502,7 @@ const MemoEditor = observer((props: Props) => {
|
||||||
<div className="flex flex-row justify-start items-center opacity-80 dark:opacity-60 space-x-2">
|
<div className="flex flex-row justify-start items-center opacity-80 dark:opacity-60 space-x-2">
|
||||||
<TagSelector editorRef={editorRef} />
|
<TagSelector editorRef={editorRef} />
|
||||||
<MarkdownMenu editorRef={editorRef} />
|
<MarkdownMenu editorRef={editorRef} />
|
||||||
<UploadResourceButton isUploadingResource={state.isUploadingResource} />
|
<UploadAttachmentButton isUploading={state.isUploadingAttachment} />
|
||||||
<AddMemoRelationPopover editorRef={editorRef} />
|
<AddMemoRelationPopover editorRef={editorRef} />
|
||||||
<LocationSelector
|
<LocationSelector
|
||||||
location={state.location}
|
location={state.location}
|
||||||
|
|
|
||||||
|
|
@ -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 { PaperclipIcon, SearchIcon, TrashIcon } from "lucide-react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import AttachmentIcon from "@/components/AttachmentIcon";
|
||||||
import Empty from "@/components/Empty";
|
import Empty from "@/components/Empty";
|
||||||
import MobileHeader from "@/components/MobileHeader";
|
import MobileHeader from "@/components/MobileHeader";
|
||||||
import ResourceIcon from "@/components/ResourceIcon";
|
|
||||||
import { attachmentServiceClient } from "@/grpcweb";
|
import { attachmentServiceClient } from "@/grpcweb";
|
||||||
import useLoading from "@/hooks/useLoading";
|
import useLoading from "@/hooks/useLoading";
|
||||||
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
|
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
|
||||||
|
|
@ -112,7 +112,7 @@ const Attachments = observer(() => {
|
||||||
return (
|
return (
|
||||||
<div key={attachment.name} className="w-24 sm:w-32 h-auto flex flex-col justify-start items-start">
|
<div key={attachment.name} className="w-24 sm:w-32 h-auto flex flex-col justify-start items-start">
|
||||||
<div className="w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-zinc-200 dark:border-zinc-900 overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80">
|
<div className="w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-zinc-200 dark:border-zinc-900 overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80">
|
||||||
<ResourceIcon resource={attachment} strokeWidth={0.5} />
|
<AttachmentIcon attachment={attachment} strokeWidth={0.5} />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full max-w-full flex flex-row justify-between items-center mt-1 px-1">
|
<div className="w-full max-w-full flex flex-row justify-between items-center mt-1 px-1">
|
||||||
<p className="text-xs shrink text-gray-400 truncate">{attachment.filename}</p>
|
<p className="text-xs shrink text-gray-400 truncate">{attachment.filename}</p>
|
||||||
|
|
@ -144,7 +144,7 @@ const Attachments = observer(() => {
|
||||||
return (
|
return (
|
||||||
<div key={attachment.name} className="w-24 sm:w-32 h-auto flex flex-col justify-start items-start">
|
<div key={attachment.name} className="w-24 sm:w-32 h-auto flex flex-col justify-start items-start">
|
||||||
<div className="w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-zinc-200 dark:border-zinc-900 overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80">
|
<div className="w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-zinc-200 dark:border-zinc-900 overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80">
|
||||||
<ResourceIcon resource={attachment} strokeWidth={0.5} />
|
<AttachmentIcon attachment={attachment} strokeWidth={0.5} />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full max-w-full flex flex-row justify-between items-center mt-1 px-1">
|
<div className="w-full max-w-full flex flex-row justify-between items-center mt-1 px-1">
|
||||||
<p className="text-xs shrink text-gray-400 truncate">{attachment.filename}</p>
|
<p className="text-xs shrink text-gray-400 truncate">{attachment.filename}</p>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue