Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
canifa_note
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Vũ Hoàng Anh
canifa_note
Commits
a4920d46
Commit
a4920d46
authored
Jun 17, 2025
by
Steven
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
refactor: attachment service part2
parent
bb5809ca
Changes
24
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
24 changed files
with
702 additions
and
418 deletions
+702
-418
http_getter.go
plugin/httpgetter/http_getter.go
+0
-3
attachment.pb.go
proto/gen/store/attachment.pb.go
+287
-0
attachment.proto
proto/store/attachment.proto
+6
-6
acl_config.go
server/router/api/v1/acl_config.go
+1
-1
attachment_service.go
server/router/api/v1/attachment_service.go
+80
-80
memo_attachment_service.go
server/router/api/v1/memo_attachment_service.go
+22
-22
memo_service.go
server/router/api/v1/memo_service.go
+6
-6
rss.go
server/router/rss/rss.go
+8
-8
runner.go
server/runner/s3presign/runner.go
+17
-17
attachment.go
store/attachment.go
+31
-31
attachment.go
store/db/mysql/attachment.go
+27
-27
attachment.go
store/db/postgres/attachment.go
+24
-24
attachment.go
store/db/sqlite/attachment.go
+25
-25
driver.go
store/driver.go
+5
-5
attachment_test.go
store/test/attachment_test.go
+13
-13
AttachmentIcon.tsx
web/src/components/AttachmentIcon.tsx
+9
-9
MemoAttachment.tsx
web/src/components/MemoAttachment.tsx
+34
-0
MemoAttachmentListView.tsx
web/src/components/MemoAttachmentListView.tsx
+2
-2
UploadAttachmentButton.tsx
...onents/MemoEditor/ActionButton/UploadAttachmentButton.tsx
+92
-0
UploadResourceButton.tsx
...mponents/MemoEditor/ActionButton/UploadResourceButton.tsx
+0
-92
AttachmentListView.tsx
web/src/components/MemoEditor/AttachmentListView.tsx
+2
-2
index.tsx
web/src/components/MemoEditor/index.tsx
+8
-8
MemoResource.tsx
web/src/components/MemoResource.tsx
+0
-34
Attachments.tsx
web/src/pages/Attachments.tsx
+3
-3
No files found.
plugin/httpgetter/http_getter.go
View file @
a4920d46
// Package httpgetter is using to get resources from url.
// * Get metadata for website;
// * Get image blob to avoid CORS;
package
httpgetter
package
httpgetter
proto/gen/store/
resource
.pb.go
→
proto/gen/store/
attachment
.pb.go
View file @
a4920d46
This diff is collapsed.
Click to expand it.
proto/store/
resource
.proto
→
proto/store/
attachment
.proto
View file @
a4920d46
...
@@ -7,17 +7,17 @@ import "store/workspace_setting.proto";
...
@@ -7,17 +7,17 @@ import "store/workspace_setting.proto";
option
go_package
=
"gen/store"
;
option
go_package
=
"gen/store"
;
enum
Resource
StorageType
{
enum
Attachment
StorageType
{
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
Resource
Payload
{
message
Attachment
Payload
{
oneof
payload
{
oneof
payload
{
S3Object
s3_object
=
1
;
S3Object
s3_object
=
1
;
}
}
...
...
server/router/api/v1/acl_config.go
View file @
a4920d46
...
@@ -18,7 +18,7 @@ var authenticationAllowlistMethods = map[string]bool{
...
@@ -18,7 +18,7 @@ var authenticationAllowlistMethods = map[string]bool{
"/memos.api.v1.MemoService/GetMemo"
:
true
,
"/memos.api.v1.MemoService/GetMemo"
:
true
,
"/memos.api.v1.MemoService/ListMemos"
:
true
,
"/memos.api.v1.MemoService/ListMemos"
:
true
,
"/memos.api.v1.MarkdownService/GetLinkMetadata"
:
true
,
"/memos.api.v1.MarkdownService/GetLinkMetadata"
:
true
,
"/memos.api.v1.
ResourceService/GetResourceBinary"
:
true
,
"/memos.api.v1.
AttachmentService/GetAttachmentBinary"
:
true
,
}
}
// isUnauthorizeAllowedMethod returns whether the method is exempted from authentication.
// isUnauthorizeAllowedMethod returns whether the method is exempted from authentication.
...
...
server/router/api/v1/attachment_service.go
View file @
a4920d46
This diff is collapsed.
Click to expand it.
server/router/api/v1/memo_attachment_service.go
View file @
a4920d46
...
@@ -22,54 +22,54 @@ func (s *APIV1Service) SetMemoAttachments(ctx context.Context, request *v1pb.Set
...
@@ -22,54 +22,54 @@ func (s *APIV1Service) SetMemoAttachments(ctx context.Context, request *v1pb.Set
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get memo"
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get memo"
)
}
}
resources
,
err
:=
s
.
Store
.
ListResources
(
ctx
,
&
store
.
FindResource
{
attachments
,
err
:=
s
.
Store
.
ListAttachments
(
ctx
,
&
store
.
FindAttachment
{
MemoID
:
&
memo
.
ID
,
MemoID
:
&
memo
.
ID
,
})
})
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to list
resource
s"
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to list
attachment
s"
)
}
}
// Delete
resource
s that are not in the request.
// Delete
attachment
s that are not in the request.
for
_
,
resource
:=
range
resource
s
{
for
_
,
attachment
:=
range
attachment
s
{
found
:=
false
found
:=
false
for
_
,
request
Resource
:=
range
request
.
Attachments
{
for
_
,
request
Attachment
:=
range
request
.
Attachments
{
request
ResourceUID
,
err
:=
ExtractAttachmentUIDFromName
(
requestResource
.
Name
)
request
AttachmentUID
,
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
==
requestResource
UID
{
if
attachment
.
UID
==
requestAttachment
UID
{
found
=
true
found
=
true
break
break
}
}
}
}
if
!
found
{
if
!
found
{
if
err
=
s
.
Store
.
Delete
Resource
(
ctx
,
&
store
.
DeleteResource
{
if
err
=
s
.
Store
.
Delete
Attachment
(
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
resource
s' memo_id in the request.
// Update
attachment
s' 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
)
}
}
temp
Resource
,
err
:=
s
.
Store
.
GetResource
(
ctx
,
&
store
.
FindResource
{
UID
:
&
resource
UID
})
temp
Attachment
,
err
:=
s
.
Store
.
GetAttachment
(
ctx
,
&
store
.
FindAttachment
{
UID
:
&
attachment
UID
})
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
.
Update
Resource
(
ctx
,
&
store
.
UpdateResource
{
if
err
:=
s
.
Store
.
Update
Attachment
(
ctx
,
&
store
.
UpdateAttachment
{
ID
:
temp
Resource
.
ID
,
ID
:
temp
Attachment
.
ID
,
MemoID
:
&
memo
.
ID
,
MemoID
:
&
memo
.
ID
,
UpdatedTs
:
&
updatedTs
,
UpdatedTs
:
&
updatedTs
,
});
err
!=
nil
{
});
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to update
resource
: %v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to update
attachment
: %v"
,
err
)
}
}
}
}
...
@@ -85,18 +85,18 @@ func (s *APIV1Service) ListMemoAttachments(ctx context.Context, request *v1pb.Li
...
@@ -85,18 +85,18 @@ func (s *APIV1Service) ListMemoAttachments(ctx context.Context, request *v1pb.Li
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get memo: %v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to get memo: %v"
,
err
)
}
}
resources
,
err
:=
s
.
Store
.
ListResources
(
ctx
,
&
store
.
FindResource
{
attachments
,
err
:=
s
.
Store
.
ListAttachments
(
ctx
,
&
store
.
FindAttachment
{
MemoID
:
&
memo
.
ID
,
MemoID
:
&
memo
.
ID
,
})
})
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to list
resource
s: %v"
,
err
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to list
attachment
s: %v"
,
err
)
}
}
response
:=
&
v1pb
.
ListMemoAttachmentsResponse
{
response
:=
&
v1pb
.
ListMemoAttachmentsResponse
{
Attachments
:
[]
*
v1pb
.
Attachment
{},
Attachments
:
[]
*
v1pb
.
Attachment
{},
}
}
for
_
,
resource
:=
range
resource
s
{
for
_
,
attachment
:=
range
attachment
s
{
response
.
Attachments
=
append
(
response
.
Attachments
,
s
.
convertAttachmentFromStore
(
ctx
,
resource
))
response
.
Attachments
=
append
(
response
.
Attachments
,
s
.
convertAttachmentFromStore
(
ctx
,
attachment
))
}
}
return
response
,
nil
return
response
,
nil
}
}
server/router/api/v1/memo_service.go
View file @
a4920d46
...
@@ -399,14 +399,14 @@ func (s *APIV1Service) DeleteMemo(ctx context.Context, request *v1pb.DeleteMemoR
...
@@ -399,14 +399,14 @@ func (s *APIV1Service) DeleteMemo(ctx context.Context, request *v1pb.DeleteMemoR
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to delete memo relations"
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to delete memo relations"
)
}
}
// Delete related
resource
s.
// Delete related
attachment
s.
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
resource
s"
)
return
nil
,
status
.
Errorf
(
codes
.
Internal
,
"failed to list
attachment
s"
)
}
}
for
_
,
resource
:=
range
resource
s
{
for
_
,
attachment
:=
range
attachment
s
{
if
err
:=
s
.
Store
.
Delete
Resource
(
ctx
,
&
store
.
DeleteResource
{
ID
:
resource
.
ID
});
err
!=
nil
{
if
err
:=
s
.
Store
.
Delete
Attachment
(
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
"
)
}
}
}
}
...
...
server/router/rss/rss.go
View file @
a4920d46
...
@@ -124,22 +124,22 @@ func (s *RSSService) generateRSSFromMemoList(ctx context.Context, memoList []*st
...
@@ -124,22 +124,22 @@ func (s *RSSService) generateRSSFromMemoList(ctx context.Context, memoList []*st
Created
:
time
.
Unix
(
memo
.
CreatedTs
,
0
),
Created
:
time
.
Unix
(
memo
.
CreatedTs
,
0
),
Id
:
link
.
Href
,
Id
:
link
.
Href
,
}
}
resources
,
err
:=
s
.
Store
.
ListResources
(
ctx
,
&
store
.
FindResource
{
attachments
,
err
:=
s
.
Store
.
ListAttachments
(
ctx
,
&
store
.
FindAttachment
{
MemoID
:
&
memo
.
ID
,
MemoID
:
&
memo
.
ID
,
})
})
if
err
!=
nil
{
if
err
!=
nil
{
return
""
,
err
return
""
,
err
}
}
if
len
(
resource
s
)
>
0
{
if
len
(
attachment
s
)
>
0
{
resource
:=
resource
s
[
0
]
attachment
:=
attachment
s
[
0
]
enclosure
:=
feeds
.
Enclosure
{}
enclosure
:=
feeds
.
Enclosure
{}
if
resource
.
StorageType
==
storepb
.
ResourceStorageType_EXTERNAL
||
resource
.
StorageType
==
storepb
.
Resource
StorageType_S3
{
if
attachment
.
StorageType
==
storepb
.
AttachmentStorageType_EXTERNAL
||
attachment
.
StorageType
==
storepb
.
Attachment
StorageType_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
}
}
}
}
...
...
server/runner/s3presign/runner.go
View file @
a4920d46
...
@@ -49,33 +49,33 @@ func (r *Runner) CheckAndPresign(ctx context.Context) {
...
@@ -49,33 +49,33 @@ func (r *Runner) CheckAndPresign(ctx context.Context) {
return
return
}
}
s3StorageType
:=
storepb
.
Resource
StorageType_S3
s3StorageType
:=
storepb
.
Attachment
StorageType_S3
// Limit
resource
s to a reasonable batch size
// Limit
attachment
s 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
resource
s for presigning"
,
"error"
,
err
)
slog
.
Error
(
"Failed to list
attachment
s for presigning"
,
"error"
,
err
)
return
return
}
}
// Break if no more
resource
s
// Break if no more
attachment
s
if
len
(
resource
s
)
==
0
{
if
len
(
attachment
s
)
==
0
{
break
break
}
}
// Process batch of
resource
s
// Process batch of
attachment
s
presignCount
:=
0
presignCount
:=
0
for
_
,
resource
:=
range
resource
s
{
for
_
,
attachment
:=
range
attachment
s
{
s3ObjectPayload
:=
resource
.
Payload
.
GetS3Object
()
s3ObjectPayload
:=
attachment
.
Payload
.
GetS3Object
()
if
s3ObjectPayload
==
nil
{
if
s3ObjectPayload
==
nil
{
continue
continue
}
}
...
@@ -105,30 +105,30 @@ func (r *Runner) CheckAndPresign(ctx context.Context) {
...
@@ -105,30 +105,30 @@ func (r *Runner) CheckAndPresign(ctx context.Context) {
presignURL
,
err
:=
s3Client
.
PresignGetObject
(
ctx
,
s3ObjectPayload
.
Key
)
presignURL
,
err
:=
s3Client
.
PresignGetObject
(
ctx
,
s3ObjectPayload
.
Key
)
if
err
!=
nil
{
if
err
!=
nil
{
slog
.
Error
(
"Failed to presign URL"
,
"error"
,
err
,
"
resourceID"
,
resource
.
ID
)
slog
.
Error
(
"Failed to presign URL"
,
"error"
,
err
,
"
attachmentID"
,
attachment
.
ID
)
continue
continue
}
}
s3ObjectPayload
.
S3Config
=
s3Config
s3ObjectPayload
.
S3Config
=
s3Config
s3ObjectPayload
.
LastPresignedTime
=
timestamppb
.
New
(
time
.
Now
())
s3ObjectPayload
.
LastPresignedTime
=
timestamppb
.
New
(
time
.
Now
())
if
err
:=
r
.
Store
.
Update
Resource
(
ctx
,
&
store
.
UpdateResource
{
if
err
:=
r
.
Store
.
Update
Attachment
(
ctx
,
&
store
.
UpdateAttachment
{
ID
:
resource
.
ID
,
ID
:
attachment
.
ID
,
Reference
:
&
presignURL
,
Reference
:
&
presignURL
,
Payload
:
&
storepb
.
Resource
Payload
{
Payload
:
&
storepb
.
Attachment
Payload
{
Payload
:
&
storepb
.
Resource
Payload_S3Object_
{
Payload
:
&
storepb
.
Attachment
Payload_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
(
resource
s
),
"presigned"
,
presignCount
)
slog
.
Info
(
"Presigned batch of S3
attachments"
,
"batchSize"
,
len
(
attachment
s
),
"presigned"
,
presignCount
)
// Move to next batch
// Move to next batch
offset
+=
len
(
resource
s
)
offset
+=
len
(
attachment
s
)
}
}
}
}
store/
resource
.go
→
store/
attachment
.go
View file @
a4920d46
...
@@ -13,10 +13,10 @@ import (
...
@@ -13,10 +13,10 @@ import (
storepb
"github.com/usememos/memos/proto/gen/store"
storepb
"github.com/usememos/memos/proto/gen/store"
)
)
type
Resource
struct
{
type
Attachment
struct
{
// ID is the system generated unique identifier for the
resource
.
// ID is the system generated unique identifier for the
attachment
.
ID
int32
ID
int32
// UID is the user defined unique identifier for the
resource
.
// UID is the user defined unique identifier for the
attachment
.
UID
string
UID
string
// Standard fields
// Standard fields
...
@@ -29,15 +29,15 @@ type Resource struct {
...
@@ -29,15 +29,15 @@ type Resource struct {
Blob
[]
byte
Blob
[]
byte
Type
string
Type
string
Size
int64
Size
int64
StorageType
storepb
.
Resource
StorageType
StorageType
storepb
.
Attachment
StorageType
Reference
string
Reference
string
Payload
*
storepb
.
Resource
Payload
Payload
*
storepb
.
Attachment
Payload
// The related memo ID.
// The related memo ID.
MemoID
*
int32
MemoID
*
int32
}
}
type
Find
Resource
struct
{
type
Find
Attachment
struct
{
GetBlob
bool
GetBlob
bool
ID
*
int32
ID
*
int32
UID
*
string
UID
*
string
...
@@ -46,35 +46,35 @@ type FindResource struct {
...
@@ -46,35 +46,35 @@ type FindResource struct {
FilenameSearch
*
string
FilenameSearch
*
string
MemoID
*
int32
MemoID
*
int32
HasRelatedMemo
bool
HasRelatedMemo
bool
StorageType
*
storepb
.
Resource
StorageType
StorageType
*
storepb
.
Attachment
StorageType
Limit
*
int
Limit
*
int
Offset
*
int
Offset
*
int
}
}
type
Update
Resource
struct
{
type
Update
Attachment
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
.
Resource
Payload
Payload
*
storepb
.
Attachment
Payload
}
}
type
Delete
Resource
struct
{
type
Delete
Attachment
struct
{
ID
int32
ID
int32
MemoID
*
int32
MemoID
*
int32
}
}
func
(
s
*
Store
)
Create
Resource
(
ctx
context
.
Context
,
create
*
Resource
)
(
*
Resource
,
error
)
{
func
(
s
*
Store
)
Create
Attachment
(
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
.
Create
Resource
(
ctx
,
create
)
return
s
.
driver
.
Create
Attachment
(
ctx
,
create
)
}
}
func
(
s
*
Store
)
List
Resources
(
ctx
context
.
Context
,
find
*
FindResource
)
([]
*
Resource
,
error
)
{
func
(
s
*
Store
)
List
Attachments
(
ctx
context
.
Context
,
find
*
FindAttachment
)
([]
*
Attachment
,
error
)
{
// Set default limits to prevent loading too many
resource
s at once
// Set default limits to prevent loading too many
attachment
s at once
if
find
.
Limit
==
nil
&&
find
.
GetBlob
{
if
find
.
Limit
==
nil
&&
find
.
GetBlob
{
// When fetching blobs, we should be especially careful with limits
// When fetching blobs, we should be especially careful with limits
defaultLimit
:=
10
defaultLimit
:=
10
...
@@ -85,41 +85,41 @@ func (s *Store) ListResources(ctx context.Context, find *FindResource) ([]*Resou
...
@@ -85,41 +85,41 @@ func (s *Store) ListResources(ctx context.Context, find *FindResource) ([]*Resou
find
.
Limit
=
&
defaultLimit
find
.
Limit
=
&
defaultLimit
}
}
return
s
.
driver
.
List
Resource
s
(
ctx
,
find
)
return
s
.
driver
.
List
Attachment
s
(
ctx
,
find
)
}
}
func
(
s
*
Store
)
Get
Resource
(
ctx
context
.
Context
,
find
*
FindResource
)
(
*
Resource
,
error
)
{
func
(
s
*
Store
)
Get
Attachment
(
ctx
context
.
Context
,
find
*
FindAttachment
)
(
*
Attachment
,
error
)
{
resources
,
err
:=
s
.
ListResource
s
(
ctx
,
find
)
attachments
,
err
:=
s
.
ListAttachment
s
(
ctx
,
find
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
if
len
(
resource
s
)
==
0
{
if
len
(
attachment
s
)
==
0
{
return
nil
,
nil
return
nil
,
nil
}
}
return
resource
s
[
0
],
nil
return
attachment
s
[
0
],
nil
}
}
func
(
s
*
Store
)
Update
Resource
(
ctx
context
.
Context
,
update
*
UpdateResource
)
error
{
func
(
s
*
Store
)
Update
Attachment
(
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
.
Update
Resource
(
ctx
,
update
)
return
s
.
driver
.
Update
Attachment
(
ctx
,
update
)
}
}
func
(
s
*
Store
)
Delete
Resource
(
ctx
context
.
Context
,
delete
*
DeleteResource
)
error
{
func
(
s
*
Store
)
Delete
Attachment
(
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
.
Resource
StorageType_LOCAL
{
if
attachment
.
StorageType
==
storepb
.
Attachment
StorageType_LOCAL
{
if
err
:=
func
()
error
{
if
err
:=
func
()
error
{
p
:=
filepath
.
FromSlash
(
resource
.
Reference
)
p
:=
filepath
.
FromSlash
(
attachment
.
Reference
)
if
!
filepath
.
IsAbs
(
p
)
{
if
!
filepath
.
IsAbs
(
p
)
{
p
=
filepath
.
Join
(
s
.
profile
.
Data
,
p
)
p
=
filepath
.
Join
(
s
.
profile
.
Data
,
p
)
}
}
...
@@ -131,9 +131,9 @@ func (s *Store) DeleteResource(ctx context.Context, delete *DeleteResource) erro
...
@@ -131,9 +131,9 @@ func (s *Store) DeleteResource(ctx context.Context, delete *DeleteResource) erro
}();
err
!=
nil
{
}();
err
!=
nil
{
return
errors
.
Wrap
(
err
,
"failed to delete local file"
)
return
errors
.
Wrap
(
err
,
"failed to delete local file"
)
}
}
}
else
if
resource
.
StorageType
==
storepb
.
Resource
StorageType_S3
{
}
else
if
attachment
.
StorageType
==
storepb
.
Attachment
StorageType_S3
{
if
err
:=
func
()
error
{
if
err
:=
func
()
error
{
s3ObjectPayload
:=
resource
.
Payload
.
GetS3Object
()
s3ObjectPayload
:=
attachment
.
Payload
.
GetS3Object
()
if
s3ObjectPayload
==
nil
{
if
s3ObjectPayload
==
nil
{
return
errors
.
Errorf
(
"No s3 object found"
)
return
errors
.
Errorf
(
"No s3 object found"
)
}
}
...
@@ -162,5 +162,5 @@ func (s *Store) DeleteResource(ctx context.Context, delete *DeleteResource) erro
...
@@ -162,5 +162,5 @@ func (s *Store) DeleteResource(ctx context.Context, delete *DeleteResource) erro
}
}
}
}
return
s
.
driver
.
Delete
Resource
(
ctx
,
delete
)
return
s
.
driver
.
Delete
Attachment
(
ctx
,
delete
)
}
}
store/db/mysql/
resource
.go
→
store/db/mysql/
attachment
.go
View file @
a4920d46
...
@@ -13,18 +13,18 @@ import (
...
@@ -13,18 +13,18 @@ import (
"github.com/usememos/memos/store"
"github.com/usememos/memos/store"
)
)
func
(
d
*
DB
)
Create
Resource
(
ctx
context
.
Context
,
create
*
store
.
Resource
)
(
*
store
.
Resource
,
error
)
{
func
(
d
*
DB
)
Create
Attachment
(
ctx
context
.
Context
,
create
*
store
.
Attachment
)
(
*
store
.
Attachment
,
error
)
{
fields
:=
[]
string
{
"`uid`"
,
"`filename`"
,
"`blob`"
,
"`type`"
,
"`size`"
,
"`creator_id`"
,
"`memo_id`"
,
"`storage_type`"
,
"`reference`"
,
"`payload`"
}
fields
:=
[]
string
{
"`uid`"
,
"`filename`"
,
"`blob`"
,
"`type`"
,
"`size`"
,
"`creator_id`"
,
"`memo_id`"
,
"`storage_type`"
,
"`reference`"
,
"`payload`"
}
placeholder
:=
[]
string
{
"?"
,
"?"
,
"?"
,
"?"
,
"?"
,
"?"
,
"?"
,
"?"
,
"?"
,
"?"
}
placeholder
:=
[]
string
{
"?"
,
"?"
,
"?"
,
"?"
,
"?"
,
"?"
,
"?"
,
"?"
,
"?"
,
"?"
}
storageType
:=
""
storageType
:=
""
if
create
.
StorageType
!=
storepb
.
ResourceStorageType_RESOURCE
_STORAGE_TYPE_UNSPECIFIED
{
if
create
.
StorageType
!=
storepb
.
AttachmentStorageType_ATTACHMENT
_STORAGE_TYPE_UNSPECIFIED
{
storageType
=
create
.
StorageType
.
String
()
storageType
=
create
.
StorageType
.
String
()
}
}
payloadString
:=
"{}"
payloadString
:=
"{}"
if
create
.
Payload
!=
nil
{
if
create
.
Payload
!=
nil
{
bytes
,
err
:=
protojson
.
Marshal
(
create
.
Payload
)
bytes
,
err
:=
protojson
.
Marshal
(
create
.
Payload
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to marshal
resource
payload"
)
return
nil
,
errors
.
Wrap
(
err
,
"failed to marshal
attachment
payload"
)
}
}
payloadString
=
string
(
bytes
)
payloadString
=
string
(
bytes
)
}
}
...
@@ -42,10 +42,10 @@ func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store
...
@@ -42,10 +42,10 @@ func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store
}
}
id32
:=
int32
(
id
)
id32
:=
int32
(
id
)
return
d
.
Get
Resource
(
ctx
,
&
store
.
FindResource
{
ID
:
&
id32
})
return
d
.
Get
Attachment
(
ctx
,
&
store
.
FindAttachment
{
ID
:
&
id32
})
}
}
func
(
d
*
DB
)
List
Resources
(
ctx
context
.
Context
,
find
*
store
.
FindResource
)
([]
*
store
.
Resource
,
error
)
{
func
(
d
*
DB
)
List
Attachments
(
ctx
context
.
Context
,
find
*
store
.
FindAttachment
)
([]
*
store
.
Attachment
,
error
)
{
where
,
args
:=
[]
string
{
"1 = 1"
},
[]
any
{}
where
,
args
:=
[]
string
{
"1 = 1"
},
[]
any
{}
if
v
:=
find
.
ID
;
v
!=
nil
{
if
v
:=
find
.
ID
;
v
!=
nil
{
...
@@ -92,43 +92,43 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
...
@@ -92,43 +92,43 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
}
}
defer
rows
.
Close
()
defer
rows
.
Close
()
list
:=
make
([]
*
store
.
Resource
,
0
)
list
:=
make
([]
*
store
.
Attachment
,
0
)
for
rows
.
Next
()
{
for
rows
.
Next
()
{
resource
:=
store
.
Resource
{}
attachment
:=
store
.
Attachment
{}
var
memoID
sql
.
NullInt32
var
memoID
sql
.
NullInt32
var
storageType
string
var
storageType
string
var
payloadBytes
[]
byte
var
payloadBytes
[]
byte
dests
:=
[]
any
{
dests
:=
[]
any
{
&
resource
.
ID
,
&
attachment
.
ID
,
&
resource
.
UID
,
&
attachment
.
UID
,
&
resource
.
Filename
,
&
attachment
.
Filename
,
&
resource
.
Type
,
&
attachment
.
Type
,
&
resource
.
Size
,
&
attachment
.
Size
,
&
resource
.
CreatorID
,
&
attachment
.
CreatorID
,
&
resource
.
CreatedTs
,
&
attachment
.
CreatedTs
,
&
resource
.
UpdatedTs
,
&
attachment
.
UpdatedTs
,
&
memoID
,
&
memoID
,
&
storageType
,
&
storageType
,
&
resource
.
Reference
,
&
attachment
.
Reference
,
&
payloadBytes
,
&
payloadBytes
,
}
}
if
find
.
GetBlob
{
if
find
.
GetBlob
{
dests
=
append
(
dests
,
&
resource
.
Blob
)
dests
=
append
(
dests
,
&
attachment
.
Blob
)
}
}
if
err
:=
rows
.
Scan
(
dests
...
);
err
!=
nil
{
if
err
:=
rows
.
Scan
(
dests
...
);
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
if
memoID
.
Valid
{
if
memoID
.
Valid
{
resource
.
MemoID
=
&
memoID
.
Int32
attachment
.
MemoID
=
&
memoID
.
Int32
}
}
resource
.
StorageType
=
storepb
.
ResourceStorageType
(
storepb
.
Resource
StorageType_value
[
storageType
])
attachment
.
StorageType
=
storepb
.
AttachmentStorageType
(
storepb
.
Attachment
StorageType_value
[
storageType
])
payload
:=
&
storepb
.
Resource
Payload
{}
payload
:=
&
storepb
.
Attachment
Payload
{}
if
err
:=
protojsonUnmarshaler
.
Unmarshal
(
payloadBytes
,
payload
);
err
!=
nil
{
if
err
:=
protojsonUnmarshaler
.
Unmarshal
(
payloadBytes
,
payload
);
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
resource
.
Payload
=
payload
attachment
.
Payload
=
payload
list
=
append
(
list
,
&
resource
)
list
=
append
(
list
,
&
attachment
)
}
}
if
err
:=
rows
.
Err
();
err
!=
nil
{
if
err
:=
rows
.
Err
();
err
!=
nil
{
...
@@ -138,8 +138,8 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
...
@@ -138,8 +138,8 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
return
list
,
nil
return
list
,
nil
}
}
func
(
d
*
DB
)
Get
Resource
(
ctx
context
.
Context
,
find
*
store
.
FindResource
)
(
*
store
.
Resource
,
error
)
{
func
(
d
*
DB
)
Get
Attachment
(
ctx
context
.
Context
,
find
*
store
.
FindAttachment
)
(
*
store
.
Attachment
,
error
)
{
list
,
err
:=
d
.
List
Resource
s
(
ctx
,
find
)
list
,
err
:=
d
.
List
Attachment
s
(
ctx
,
find
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
...
@@ -150,7 +150,7 @@ func (d *DB) GetResource(ctx context.Context, find *store.FindResource) (*store.
...
@@ -150,7 +150,7 @@ func (d *DB) GetResource(ctx context.Context, find *store.FindResource) (*store.
return
list
[
0
],
nil
return
list
[
0
],
nil
}
}
func
(
d
*
DB
)
Update
Resource
(
ctx
context
.
Context
,
update
*
store
.
UpdateResource
)
error
{
func
(
d
*
DB
)
Update
Attachment
(
ctx
context
.
Context
,
update
*
store
.
UpdateAttachment
)
error
{
set
,
args
:=
[]
string
{},
[]
any
{}
set
,
args
:=
[]
string
{},
[]
any
{}
if
v
:=
update
.
UID
;
v
!=
nil
{
if
v
:=
update
.
UID
;
v
!=
nil
{
...
@@ -171,7 +171,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
...
@@ -171,7 +171,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
if
v
:=
update
.
Payload
;
v
!=
nil
{
if
v
:=
update
.
Payload
;
v
!=
nil
{
bytes
,
err
:=
protojson
.
Marshal
(
v
)
bytes
,
err
:=
protojson
.
Marshal
(
v
)
if
err
!=
nil
{
if
err
!=
nil
{
return
errors
.
Wrap
(
err
,
"failed to marshal
resource
payload"
)
return
errors
.
Wrap
(
err
,
"failed to marshal
attachment
payload"
)
}
}
set
,
args
=
append
(
set
,
"`payload` = ?"
),
append
(
args
,
string
(
bytes
))
set
,
args
=
append
(
set
,
"`payload` = ?"
),
append
(
args
,
string
(
bytes
))
}
}
...
@@ -188,7 +188,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
...
@@ -188,7 +188,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
return
nil
return
nil
}
}
func
(
d
*
DB
)
Delete
Resource
(
ctx
context
.
Context
,
delete
*
store
.
DeleteResource
)
error
{
func
(
d
*
DB
)
Delete
Attachment
(
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
{
...
...
store/db/postgres/
resource
.go
→
store/db/postgres/
attachment
.go
View file @
a4920d46
...
@@ -13,17 +13,17 @@ import (
...
@@ -13,17 +13,17 @@ import (
"github.com/usememos/memos/store"
"github.com/usememos/memos/store"
)
)
func
(
d
*
DB
)
Create
Resource
(
ctx
context
.
Context
,
create
*
store
.
Resource
)
(
*
store
.
Resource
,
error
)
{
func
(
d
*
DB
)
Create
Attachment
(
ctx
context
.
Context
,
create
*
store
.
Attachment
)
(
*
store
.
Attachment
,
error
)
{
fields
:=
[]
string
{
"uid"
,
"filename"
,
"blob"
,
"type"
,
"size"
,
"creator_id"
,
"memo_id"
,
"storage_type"
,
"reference"
,
"payload"
}
fields
:=
[]
string
{
"uid"
,
"filename"
,
"blob"
,
"type"
,
"size"
,
"creator_id"
,
"memo_id"
,
"storage_type"
,
"reference"
,
"payload"
}
storageType
:=
""
storageType
:=
""
if
create
.
StorageType
!=
storepb
.
ResourceStorageType_RESOURCE
_STORAGE_TYPE_UNSPECIFIED
{
if
create
.
StorageType
!=
storepb
.
AttachmentStorageType_ATTACHMENT
_STORAGE_TYPE_UNSPECIFIED
{
storageType
=
create
.
StorageType
.
String
()
storageType
=
create
.
StorageType
.
String
()
}
}
payloadString
:=
"{}"
payloadString
:=
"{}"
if
create
.
Payload
!=
nil
{
if
create
.
Payload
!=
nil
{
bytes
,
err
:=
protojson
.
Marshal
(
create
.
Payload
)
bytes
,
err
:=
protojson
.
Marshal
(
create
.
Payload
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to marshal
resource
payload"
)
return
nil
,
errors
.
Wrap
(
err
,
"failed to marshal
attachment
payload"
)
}
}
payloadString
=
string
(
bytes
)
payloadString
=
string
(
bytes
)
}
}
...
@@ -36,7 +36,7 @@ func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store
...
@@ -36,7 +36,7 @@ func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store
return
create
,
nil
return
create
,
nil
}
}
func
(
d
*
DB
)
List
Resources
(
ctx
context
.
Context
,
find
*
store
.
FindResource
)
([]
*
store
.
Resource
,
error
)
{
func
(
d
*
DB
)
List
Attachments
(
ctx
context
.
Context
,
find
*
store
.
FindAttachment
)
([]
*
store
.
Attachment
,
error
)
{
where
,
args
:=
[]
string
{
"1 = 1"
},
[]
any
{}
where
,
args
:=
[]
string
{
"1 = 1"
},
[]
any
{}
if
v
:=
find
.
ID
;
v
!=
nil
{
if
v
:=
find
.
ID
;
v
!=
nil
{
...
@@ -89,43 +89,43 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
...
@@ -89,43 +89,43 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
}
}
defer
rows
.
Close
()
defer
rows
.
Close
()
list
:=
make
([]
*
store
.
Resource
,
0
)
list
:=
make
([]
*
store
.
Attachment
,
0
)
for
rows
.
Next
()
{
for
rows
.
Next
()
{
resource
:=
store
.
Resource
{}
attachment
:=
store
.
Attachment
{}
var
memoID
sql
.
NullInt32
var
memoID
sql
.
NullInt32
var
storageType
string
var
storageType
string
var
payloadBytes
[]
byte
var
payloadBytes
[]
byte
dests
:=
[]
any
{
dests
:=
[]
any
{
&
resource
.
ID
,
&
attachment
.
ID
,
&
resource
.
UID
,
&
attachment
.
UID
,
&
resource
.
Filename
,
&
attachment
.
Filename
,
&
resource
.
Type
,
&
attachment
.
Type
,
&
resource
.
Size
,
&
attachment
.
Size
,
&
resource
.
CreatorID
,
&
attachment
.
CreatorID
,
&
resource
.
CreatedTs
,
&
attachment
.
CreatedTs
,
&
resource
.
UpdatedTs
,
&
attachment
.
UpdatedTs
,
&
memoID
,
&
memoID
,
&
storageType
,
&
storageType
,
&
resource
.
Reference
,
&
attachment
.
Reference
,
&
payloadBytes
,
&
payloadBytes
,
}
}
if
find
.
GetBlob
{
if
find
.
GetBlob
{
dests
=
append
(
dests
,
&
resource
.
Blob
)
dests
=
append
(
dests
,
&
attachment
.
Blob
)
}
}
if
err
:=
rows
.
Scan
(
dests
...
);
err
!=
nil
{
if
err
:=
rows
.
Scan
(
dests
...
);
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
if
memoID
.
Valid
{
if
memoID
.
Valid
{
resource
.
MemoID
=
&
memoID
.
Int32
attachment
.
MemoID
=
&
memoID
.
Int32
}
}
resource
.
StorageType
=
storepb
.
ResourceStorageType
(
storepb
.
Resource
StorageType_value
[
storageType
])
attachment
.
StorageType
=
storepb
.
AttachmentStorageType
(
storepb
.
Attachment
StorageType_value
[
storageType
])
payload
:=
&
storepb
.
Resource
Payload
{}
payload
:=
&
storepb
.
Attachment
Payload
{}
if
err
:=
protojsonUnmarshaler
.
Unmarshal
(
payloadBytes
,
payload
);
err
!=
nil
{
if
err
:=
protojsonUnmarshaler
.
Unmarshal
(
payloadBytes
,
payload
);
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
resource
.
Payload
=
payload
attachment
.
Payload
=
payload
list
=
append
(
list
,
&
resource
)
list
=
append
(
list
,
&
attachment
)
}
}
if
err
:=
rows
.
Err
();
err
!=
nil
{
if
err
:=
rows
.
Err
();
err
!=
nil
{
...
@@ -135,7 +135,7 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
...
@@ -135,7 +135,7 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
return
list
,
nil
return
list
,
nil
}
}
func
(
d
*
DB
)
Update
Resource
(
ctx
context
.
Context
,
update
*
store
.
UpdateResource
)
error
{
func
(
d
*
DB
)
Update
Attachment
(
ctx
context
.
Context
,
update
*
store
.
UpdateAttachment
)
error
{
set
,
args
:=
[]
string
{},
[]
any
{}
set
,
args
:=
[]
string
{},
[]
any
{}
if
v
:=
update
.
UID
;
v
!=
nil
{
if
v
:=
update
.
UID
;
v
!=
nil
{
...
@@ -156,7 +156,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
...
@@ -156,7 +156,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
if
v
:=
update
.
Payload
;
v
!=
nil
{
if
v
:=
update
.
Payload
;
v
!=
nil
{
bytes
,
err
:=
protojson
.
Marshal
(
v
)
bytes
,
err
:=
protojson
.
Marshal
(
v
)
if
err
!=
nil
{
if
err
!=
nil
{
return
errors
.
Wrap
(
err
,
"failed to marshal
resource
payload"
)
return
errors
.
Wrap
(
err
,
"failed to marshal
attachment
payload"
)
}
}
set
,
args
=
append
(
set
,
"payload = "
+
placeholder
(
len
(
args
)
+
1
)),
append
(
args
,
string
(
bytes
))
set
,
args
=
append
(
set
,
"payload = "
+
placeholder
(
len
(
args
)
+
1
)),
append
(
args
,
string
(
bytes
))
}
}
...
@@ -173,7 +173,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
...
@@ -173,7 +173,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
return
nil
return
nil
}
}
func
(
d
*
DB
)
Delete
Resource
(
ctx
context
.
Context
,
delete
*
store
.
DeleteResource
)
error
{
func
(
d
*
DB
)
Delete
Attachment
(
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
{
...
...
store/db/sqlite/
resource
.go
→
store/db/sqlite/
attachment
.go
View file @
a4920d46
...
@@ -13,18 +13,18 @@ import (
...
@@ -13,18 +13,18 @@ import (
"github.com/usememos/memos/store"
"github.com/usememos/memos/store"
)
)
func
(
d
*
DB
)
Create
Resource
(
ctx
context
.
Context
,
create
*
store
.
Resource
)
(
*
store
.
Resource
,
error
)
{
func
(
d
*
DB
)
Create
Attachment
(
ctx
context
.
Context
,
create
*
store
.
Attachment
)
(
*
store
.
Attachment
,
error
)
{
fields
:=
[]
string
{
"`uid`"
,
"`filename`"
,
"`blob`"
,
"`type`"
,
"`size`"
,
"`creator_id`"
,
"`memo_id`"
,
"`storage_type`"
,
"`reference`"
,
"`payload`"
}
fields
:=
[]
string
{
"`uid`"
,
"`filename`"
,
"`blob`"
,
"`type`"
,
"`size`"
,
"`creator_id`"
,
"`memo_id`"
,
"`storage_type`"
,
"`reference`"
,
"`payload`"
}
placeholder
:=
[]
string
{
"?"
,
"?"
,
"?"
,
"?"
,
"?"
,
"?"
,
"?"
,
"?"
,
"?"
,
"?"
}
placeholder
:=
[]
string
{
"?"
,
"?"
,
"?"
,
"?"
,
"?"
,
"?"
,
"?"
,
"?"
,
"?"
,
"?"
}
storageType
:=
""
storageType
:=
""
if
create
.
StorageType
!=
storepb
.
ResourceStorageType_RESOURCE
_STORAGE_TYPE_UNSPECIFIED
{
if
create
.
StorageType
!=
storepb
.
AttachmentStorageType_ATTACHMENT
_STORAGE_TYPE_UNSPECIFIED
{
storageType
=
create
.
StorageType
.
String
()
storageType
=
create
.
StorageType
.
String
()
}
}
payloadString
:=
"{}"
payloadString
:=
"{}"
if
create
.
Payload
!=
nil
{
if
create
.
Payload
!=
nil
{
bytes
,
err
:=
protojson
.
Marshal
(
create
.
Payload
)
bytes
,
err
:=
protojson
.
Marshal
(
create
.
Payload
)
if
err
!=
nil
{
if
err
!=
nil
{
return
nil
,
errors
.
Wrap
(
err
,
"failed to marshal
resource
payload"
)
return
nil
,
errors
.
Wrap
(
err
,
"failed to marshal
attachment
payload"
)
}
}
payloadString
=
string
(
bytes
)
payloadString
=
string
(
bytes
)
}
}
...
@@ -38,7 +38,7 @@ func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store
...
@@ -38,7 +38,7 @@ func (d *DB) CreateResource(ctx context.Context, create *store.Resource) (*store
return
create
,
nil
return
create
,
nil
}
}
func
(
d
*
DB
)
List
Resources
(
ctx
context
.
Context
,
find
*
store
.
FindResource
)
([]
*
store
.
Resource
,
error
)
{
func
(
d
*
DB
)
List
Attachments
(
ctx
context
.
Context
,
find
*
store
.
FindAttachment
)
([]
*
store
.
Attachment
,
error
)
{
where
,
args
:=
[]
string
{
"1 = 1"
},
[]
any
{}
where
,
args
:=
[]
string
{
"1 = 1"
},
[]
any
{}
if
v
:=
find
.
ID
;
v
!=
nil
{
if
v
:=
find
.
ID
;
v
!=
nil
{
...
@@ -85,43 +85,43 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
...
@@ -85,43 +85,43 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
}
}
defer
rows
.
Close
()
defer
rows
.
Close
()
list
:=
make
([]
*
store
.
Resource
,
0
)
list
:=
make
([]
*
store
.
Attachment
,
0
)
for
rows
.
Next
()
{
for
rows
.
Next
()
{
resource
:=
store
.
Resource
{}
attachment
:=
store
.
Attachment
{}
var
memoID
sql
.
NullInt32
var
memoID
sql
.
NullInt32
var
storageType
string
var
storageType
string
var
payloadBytes
[]
byte
var
payloadBytes
[]
byte
dests
:=
[]
any
{
dests
:=
[]
any
{
&
resource
.
ID
,
&
attachment
.
ID
,
&
resource
.
UID
,
&
attachment
.
UID
,
&
resource
.
Filename
,
&
attachment
.
Filename
,
&
resource
.
Type
,
&
attachment
.
Type
,
&
resource
.
Size
,
&
attachment
.
Size
,
&
resource
.
CreatorID
,
&
attachment
.
CreatorID
,
&
resource
.
CreatedTs
,
&
attachment
.
CreatedTs
,
&
resource
.
UpdatedTs
,
&
attachment
.
UpdatedTs
,
&
memoID
,
&
memoID
,
&
storageType
,
&
storageType
,
&
resource
.
Reference
,
&
attachment
.
Reference
,
&
payloadBytes
,
&
payloadBytes
,
}
}
if
find
.
GetBlob
{
if
find
.
GetBlob
{
dests
=
append
(
dests
,
&
resource
.
Blob
)
dests
=
append
(
dests
,
&
attachment
.
Blob
)
}
}
if
err
:=
rows
.
Scan
(
dests
...
);
err
!=
nil
{
if
err
:=
rows
.
Scan
(
dests
...
);
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
if
memoID
.
Valid
{
if
memoID
.
Valid
{
resource
.
MemoID
=
&
memoID
.
Int32
attachment
.
MemoID
=
&
memoID
.
Int32
}
}
resource
.
StorageType
=
storepb
.
ResourceStorageType
(
storepb
.
Resource
StorageType_value
[
storageType
])
attachment
.
StorageType
=
storepb
.
AttachmentStorageType
(
storepb
.
Attachment
StorageType_value
[
storageType
])
payload
:=
&
storepb
.
Resource
Payload
{}
payload
:=
&
storepb
.
Attachment
Payload
{}
if
err
:=
protojsonUnmarshaler
.
Unmarshal
(
payloadBytes
,
payload
);
err
!=
nil
{
if
err
:=
protojsonUnmarshaler
.
Unmarshal
(
payloadBytes
,
payload
);
err
!=
nil
{
return
nil
,
err
return
nil
,
err
}
}
resource
.
Payload
=
payload
attachment
.
Payload
=
payload
list
=
append
(
list
,
&
resource
)
list
=
append
(
list
,
&
attachment
)
}
}
if
err
:=
rows
.
Err
();
err
!=
nil
{
if
err
:=
rows
.
Err
();
err
!=
nil
{
...
@@ -131,7 +131,7 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
...
@@ -131,7 +131,7 @@ func (d *DB) ListResources(ctx context.Context, find *store.FindResource) ([]*st
return
list
,
nil
return
list
,
nil
}
}
func
(
d
*
DB
)
Update
Resource
(
ctx
context
.
Context
,
update
*
store
.
UpdateResource
)
error
{
func
(
d
*
DB
)
Update
Attachment
(
ctx
context
.
Context
,
update
*
store
.
UpdateAttachment
)
error
{
set
,
args
:=
[]
string
{},
[]
any
{}
set
,
args
:=
[]
string
{},
[]
any
{}
if
v
:=
update
.
UID
;
v
!=
nil
{
if
v
:=
update
.
UID
;
v
!=
nil
{
...
@@ -152,7 +152,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
...
@@ -152,7 +152,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
if
v
:=
update
.
Payload
;
v
!=
nil
{
if
v
:=
update
.
Payload
;
v
!=
nil
{
bytes
,
err
:=
protojson
.
Marshal
(
v
)
bytes
,
err
:=
protojson
.
Marshal
(
v
)
if
err
!=
nil
{
if
err
!=
nil
{
return
errors
.
Wrap
(
err
,
"failed to marshal
resource
payload"
)
return
errors
.
Wrap
(
err
,
"failed to marshal
attachment
payload"
)
}
}
set
,
args
=
append
(
set
,
"`payload` = ?"
),
append
(
args
,
string
(
bytes
))
set
,
args
=
append
(
set
,
"`payload` = ?"
),
append
(
args
,
string
(
bytes
))
}
}
...
@@ -161,7 +161,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
...
@@ -161,7 +161,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
stmt
:=
"UPDATE `resource` SET "
+
strings
.
Join
(
set
,
", "
)
+
" WHERE `id` = ?"
stmt
:=
"UPDATE `resource` SET "
+
strings
.
Join
(
set
,
", "
)
+
" WHERE `id` = ?"
result
,
err
:=
d
.
db
.
ExecContext
(
ctx
,
stmt
,
args
...
)
result
,
err
:=
d
.
db
.
ExecContext
(
ctx
,
stmt
,
args
...
)
if
err
!=
nil
{
if
err
!=
nil
{
return
errors
.
Wrap
(
err
,
"failed to update
resource
"
)
return
errors
.
Wrap
(
err
,
"failed to update
attachment
"
)
}
}
if
_
,
err
:=
result
.
RowsAffected
();
err
!=
nil
{
if
_
,
err
:=
result
.
RowsAffected
();
err
!=
nil
{
return
err
return
err
...
@@ -169,7 +169,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
...
@@ -169,7 +169,7 @@ func (d *DB) UpdateResource(ctx context.Context, update *store.UpdateResource) e
return
nil
return
nil
}
}
func
(
d
*
DB
)
Delete
Resource
(
ctx
context
.
Context
,
delete
*
store
.
DeleteResource
)
error
{
func
(
d
*
DB
)
Delete
Attachment
(
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
{
...
...
store/driver.go
View file @
a4920d46
...
@@ -25,11 +25,11 @@ type Driver interface {
...
@@ -25,11 +25,11 @@ type Driver interface {
CreateActivity
(
ctx
context
.
Context
,
create
*
Activity
)
(
*
Activity
,
error
)
CreateActivity
(
ctx
context
.
Context
,
create
*
Activity
)
(
*
Activity
,
error
)
ListActivities
(
ctx
context
.
Context
,
find
*
FindActivity
)
([]
*
Activity
,
error
)
ListActivities
(
ctx
context
.
Context
,
find
*
FindActivity
)
([]
*
Activity
,
error
)
//
Resource
model related methods.
//
Attachment
model related methods.
Create
Resource
(
ctx
context
.
Context
,
create
*
Resource
)
(
*
Resource
,
error
)
Create
Attachment
(
ctx
context
.
Context
,
create
*
Attachment
)
(
*
Attachment
,
error
)
List
Resources
(
ctx
context
.
Context
,
find
*
FindResource
)
([]
*
Resource
,
error
)
List
Attachments
(
ctx
context
.
Context
,
find
*
FindAttachment
)
([]
*
Attachment
,
error
)
Update
Resource
(
ctx
context
.
Context
,
update
*
UpdateResource
)
error
Update
Attachment
(
ctx
context
.
Context
,
update
*
UpdateAttachment
)
error
Delete
Resource
(
ctx
context
.
Context
,
delete
*
DeleteResource
)
error
Delete
Attachment
(
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
)
...
...
store/test/
resource
_test.go
→
store/test/
attachment
_test.go
View file @
a4920d46
...
@@ -10,10 +10,10 @@ import (
...
@@ -10,10 +10,10 @@ import (
"github.com/usememos/memos/store"
"github.com/usememos/memos/store"
)
)
func
Test
Resource
Store
(
t
*
testing
.
T
)
{
func
Test
Attachment
Store
(
t
*
testing
.
T
)
{
ctx
:=
context
.
Background
()
ctx
:=
context
.
Background
()
ts
:=
NewTestingStore
(
ctx
,
t
)
ts
:=
NewTestingStore
(
ctx
,
t
)
_
,
err
:=
ts
.
Create
Resource
(
ctx
,
&
store
.
Resource
{
_
,
err
:=
ts
.
Create
Attachment
(
ctx
,
&
store
.
Attachment
{
UID
:
shortuuid
.
New
(),
UID
:
shortuuid
.
New
(),
CreatorID
:
101
,
CreatorID
:
101
,
Filename
:
"test.epub"
,
Filename
:
"test.epub"
,
...
@@ -25,39 +25,39 @@ func TestResourceStore(t *testing.T) {
...
@@ -25,39 +25,39 @@ func TestResourceStore(t *testing.T) {
correctFilename
:=
"test.epub"
correctFilename
:=
"test.epub"
incorrectFilename
:=
"test.png"
incorrectFilename
:=
"test.png"
resource
,
err
:=
ts
.
GetResource
(
ctx
,
&
store
.
FindResource
{
attachment
,
err
:=
ts
.
GetAttachment
(
ctx
,
&
store
.
FindAttachment
{
Filename
:
&
correctFilename
,
Filename
:
&
correctFilename
,
})
})
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
require
.
Equal
(
t
,
correctFilename
,
resource
.
Filename
)
require
.
Equal
(
t
,
correctFilename
,
attachment
.
Filename
)
require
.
Equal
(
t
,
int32
(
1
),
resource
.
ID
)
require
.
Equal
(
t
,
int32
(
1
),
attachment
.
ID
)
notFound
Resource
,
err
:=
ts
.
GetResource
(
ctx
,
&
store
.
FindResource
{
notFound
Attachment
,
err
:=
ts
.
GetAttachment
(
ctx
,
&
store
.
FindAttachment
{
Filename
:
&
incorrectFilename
,
Filename
:
&
incorrectFilename
,
})
})
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
require
.
Nil
(
t
,
notFound
Resource
)
require
.
Nil
(
t
,
notFound
Attachment
)
var
correctCreatorID
int32
=
101
var
correctCreatorID
int32
=
101
var
incorrectCreatorID
int32
=
102
var
incorrectCreatorID
int32
=
102
_
,
err
=
ts
.
Get
Resource
(
ctx
,
&
store
.
FindResource
{
_
,
err
=
ts
.
Get
Attachment
(
ctx
,
&
store
.
FindAttachment
{
CreatorID
:
&
correctCreatorID
,
CreatorID
:
&
correctCreatorID
,
})
})
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
notFound
Resource
,
err
=
ts
.
GetResource
(
ctx
,
&
store
.
FindResource
{
notFound
Attachment
,
err
=
ts
.
GetAttachment
(
ctx
,
&
store
.
FindAttachment
{
CreatorID
:
&
incorrectCreatorID
,
CreatorID
:
&
incorrectCreatorID
,
})
})
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
require
.
Nil
(
t
,
notFound
Resource
)
require
.
Nil
(
t
,
notFound
Attachment
)
err
=
ts
.
Delete
Resource
(
ctx
,
&
store
.
DeleteResource
{
err
=
ts
.
Delete
Attachment
(
ctx
,
&
store
.
DeleteAttachment
{
ID
:
1
,
ID
:
1
,
})
})
require
.
NoError
(
t
,
err
)
require
.
NoError
(
t
,
err
)
err
=
ts
.
Delete
Resource
(
ctx
,
&
store
.
DeleteResource
{
err
=
ts
.
Delete
Attachment
(
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
()
}
}
web/src/components/
Resource
Icon.tsx
→
web/src/components/
Attachment
Icon.tsx
View file @
a4920d46
...
@@ -17,15 +17,15 @@ import showPreviewImageDialog from "./PreviewImageDialog";
...
@@ -17,15 +17,15 @@ import showPreviewImageDialog from "./PreviewImageDialog";
import
SquareDiv
from
"./kit/SquareDiv"
;
import
SquareDiv
from
"./kit/SquareDiv"
;
interface
Props
{
interface
Props
{
resource
:
Attachment
;
attachment
:
Attachment
;
className
?:
string
;
className
?:
string
;
strokeWidth
?:
number
;
strokeWidth
?:
number
;
}
}
const
Resource
Icon
=
(
props
:
Props
)
=>
{
const
Attachment
Icon
=
(
props
:
Props
)
=>
{
const
{
resource
}
=
props
;
const
{
attachment
}
=
props
;
const
resourceType
=
getAttachmentType
(
resource
);
const
resourceType
=
getAttachmentType
(
attachment
);
const
resourceUrl
=
getAttachmentUrl
(
resource
);
const
resourceUrl
=
getAttachmentUrl
(
attachment
);
const
className
=
cn
(
"w-full h-auto"
,
props
.
className
);
const
className
=
cn
(
"w-full h-auto"
,
props
.
className
);
const
strokeWidth
=
props
.
strokeWidth
;
const
strokeWidth
=
props
.
strokeWidth
;
...
@@ -38,7 +38,7 @@ const ResourceIcon = (props: Props) => {
...
@@ -38,7 +38,7 @@ const ResourceIcon = (props: Props) => {
<
SquareDiv
className=
{
cn
(
className
,
"flex items-center justify-center overflow-clip"
)
}
>
<
SquareDiv
className=
{
cn
(
className
,
"flex items-center justify-center overflow-clip"
)
}
>
<
img
<
img
className=
"min-w-full min-h-full object-cover"
className=
"min-w-full min-h-full object-cover"
src=
{
resource
.
externalLink
?
resourceUrl
:
resourceUrl
+
"?thumbnail=true"
}
src=
{
attachment
.
externalLink
?
resourceUrl
:
resourceUrl
+
"?thumbnail=true"
}
onClick=
{
()
=>
showPreviewImageDialog
(
resourceUrl
)
}
onClick=
{
()
=>
showPreviewImageDialog
(
resourceUrl
)
}
decoding=
"async"
decoding=
"async"
loading=
"lazy"
loading=
"lazy"
...
@@ -47,7 +47,7 @@ const ResourceIcon = (props: Props) => {
...
@@ -47,7 +47,7 @@ const ResourceIcon = (props: Props) => {
);
);
}
}
const
get
Resource
Icon
=
()
=>
{
const
get
Attachment
Icon
=
()
=>
{
switch
(
resourceType
)
{
switch
(
resourceType
)
{
case
"video/*"
:
case
"video/*"
:
return
<
FileVideo2Icon
strokeWidth=
{
strokeWidth
}
className=
"w-full h-auto"
/>;
return
<
FileVideo2Icon
strokeWidth=
{
strokeWidth
}
className=
"w-full h-auto"
/>;
...
@@ -74,9 +74,9 @@ const ResourceIcon = (props: Props) => {
...
@@ -74,9 +74,9 @@ const ResourceIcon = (props: Props) => {
return
(
return
(
<
div
onClick=
{
previewResource
}
className=
{
cn
(
className
,
"max-w-16 opacity-50"
)
}
>
<
div
onClick=
{
previewResource
}
className=
{
cn
(
className
,
"max-w-16 opacity-50"
)
}
>
{
get
Resource
Icon
()
}
{
get
Attachment
Icon
()
}
</
div
>
</
div
>
);
);
};
};
export
default
React
.
memo
(
Resource
Icon
);
export
default
React
.
memo
(
Attachment
Icon
);
web/src/components/MemoAttachment.tsx
0 → 100644
View file @
a4920d46
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
;
web/src/components/MemoAttachmentListView.tsx
View file @
a4920d46
...
@@ -2,7 +2,7 @@ import { memo } from "react";
...
@@ -2,7 +2,7 @@ import { memo } from "react";
import
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service"
;
import
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service"
;
import
{
cn
}
from
"@/utils"
;
import
{
cn
}
from
"@/utils"
;
import
{
getAttachmentType
,
getAttachmentUrl
}
from
"@/utils/attachment"
;
import
{
getAttachmentType
,
getAttachmentUrl
}
from
"@/utils/attachment"
;
import
Memo
Resource
from
"./MemoResource
"
;
import
Memo
Attachment
from
"./MemoAttachment
"
;
import
showPreviewImageDialog
from
"./PreviewImageDialog"
;
import
showPreviewImageDialog
from
"./PreviewImageDialog"
;
const
MemoAttachmentListView
=
({
attachments
=
[]
}:
{
attachments
:
Attachment
[]
})
=>
{
const
MemoAttachmentListView
=
({
attachments
=
[]
}:
{
attachments
:
Attachment
[]
})
=>
{
...
@@ -78,7 +78,7 @@ const MemoAttachmentListView = ({ attachments = [] }: { attachments: Attachment[
...
@@ -78,7 +78,7 @@ const MemoAttachmentListView = ({ attachments = [] }: { attachments: Attachment[
return
(
return
(
<
div
className=
"w-full flex flex-row justify-start overflow-auto gap-2"
>
<
div
className=
"w-full flex flex-row justify-start overflow-auto gap-2"
>
{
otherAttachments
.
map
((
attachment
)
=>
(
{
otherAttachments
.
map
((
attachment
)
=>
(
<
Memo
Resource
key=
{
attachment
.
name
}
resource
=
{
attachment
}
/>
<
Memo
Attachment
key=
{
attachment
.
name
}
attachment
=
{
attachment
}
/>
))
}
))
}
</
div
>
</
div
>
);
);
...
...
web/src/components/MemoEditor/ActionButton/UploadAttachmentButton.tsx
0 → 100644
View file @
a4920d46
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
;
web/src/components/MemoEditor/ActionButton/UploadResourceButton.tsx
View file @
a4920d46
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
;
web/src/components/MemoEditor/AttachmentListView.tsx
View file @
a4920d46
...
@@ -2,7 +2,7 @@ import { DndContext, closestCenter, MouseSensor, TouchSensor, useSensor, useSens
...
@@ -2,7 +2,7 @@ import { DndContext, closestCenter, MouseSensor, TouchSensor, useSensor, useSens
import
{
arrayMove
,
SortableContext
,
verticalListSortingStrategy
}
from
"@dnd-kit/sortable"
;
import
{
arrayMove
,
SortableContext
,
verticalListSortingStrategy
}
from
"@dnd-kit/sortable"
;
import
{
XIcon
}
from
"lucide-react"
;
import
{
XIcon
}
from
"lucide-react"
;
import
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service"
;
import
{
Attachment
}
from
"@/types/proto/api/v1/attachment_service"
;
import
ResourceIcon
from
"../Resource
Icon"
;
import
AttachmentIcon
from
"../Attachment
Icon"
;
import
SortableItem
from
"./SortableItem"
;
import
SortableItem
from
"./SortableItem"
;
interface
Props
{
interface
Props
{
...
@@ -41,7 +41,7 @@ const AttachmentListView = (props: Props) => {
...
@@ -41,7 +41,7 @@ const AttachmentListView = (props: Props) => {
className=
"max-w-full w-auto flex flex-row justify-start items-center flex-nowrap gap-x-1 bg-zinc-100 dark:bg-zinc-900 px-2 py-1 rounded hover:shadow-sm text-gray-500 dark:text-gray-400"
className=
"max-w-full w-auto flex flex-row justify-start items-center flex-nowrap gap-x-1 bg-zinc-100 dark:bg-zinc-900 px-2 py-1 rounded hover:shadow-sm text-gray-500 dark:text-gray-400"
>
>
<
SortableItem
id=
{
attachment
.
name
}
className=
"flex flex-row justify-start items-center gap-x-1"
>
<
SortableItem
id=
{
attachment
.
name
}
className=
"flex flex-row justify-start items-center gap-x-1"
>
<
ResourceIcon
resource
=
{
attachment
}
className=
"w-4! h-4! opacity-100!"
/>
<
AttachmentIcon
attachment
=
{
attachment
}
className=
"w-4! h-4! opacity-100!"
/>
<
span
className=
"text-sm max-w-32 truncate"
>
{
attachment
.
filename
}
</
span
>
<
span
className=
"text-sm max-w-32 truncate"
>
{
attachment
.
filename
}
</
span
>
</
SortableItem
>
</
SortableItem
>
<
button
className=
"shrink-0"
onClick=
{
()
=>
handleDeleteAttachment
(
attachment
.
name
)
}
>
<
button
className=
"shrink-0"
onClick=
{
()
=>
handleDeleteAttachment
(
attachment
.
name
)
}
>
...
...
web/src/components/MemoEditor/index.tsx
View file @
a4920d46
...
@@ -25,7 +25,7 @@ import AddMemoRelationPopover from "./ActionButton/AddMemoRelationPopover";
...
@@ -25,7 +25,7 @@ import AddMemoRelationPopover from "./ActionButton/AddMemoRelationPopover";
import
LocationSelector
from
"./ActionButton/LocationSelector"
;
import
LocationSelector
from
"./ActionButton/LocationSelector"
;
import
MarkdownMenu
from
"./ActionButton/MarkdownMenu"
;
import
MarkdownMenu
from
"./ActionButton/MarkdownMenu"
;
import
TagSelector
from
"./ActionButton/TagSelector"
;
import
TagSelector
from
"./ActionButton/TagSelector"
;
import
Upload
ResourceButton
from
"./ActionButton/UploadResource
Button"
;
import
Upload
AttachmentButton
from
"./ActionButton/UploadAttachment
Button"
;
import
VisibilitySelector
from
"./ActionButton/VisibilitySelector"
;
import
VisibilitySelector
from
"./ActionButton/VisibilitySelector"
;
import
AttachmentListView
from
"./AttachmentListView"
;
import
AttachmentListView
from
"./AttachmentListView"
;
import
Editor
,
{
EditorRefActions
}
from
"./Editor"
;
import
Editor
,
{
EditorRefActions
}
from
"./Editor"
;
...
@@ -51,7 +51,7 @@ interface State {
...
@@ -51,7 +51,7 @@ interface State {
attachmentList
:
Attachment
[];
attachmentList
:
Attachment
[];
relationList
:
MemoRelation
[];
relationList
:
MemoRelation
[];
location
:
Location
|
undefined
;
location
:
Location
|
undefined
;
isUploading
Resource
:
boolean
;
isUploading
Attachment
:
boolean
;
isRequesting
:
boolean
;
isRequesting
:
boolean
;
isComposing
:
boolean
;
isComposing
:
boolean
;
isDraggingFile
:
boolean
;
isDraggingFile
:
boolean
;
...
@@ -67,7 +67,7 @@ const MemoEditor = observer((props: Props) => {
...
@@ -67,7 +67,7 @@ const MemoEditor = observer((props: Props) => {
attachmentList
:
[],
attachmentList
:
[],
relationList
:
[],
relationList
:
[],
location
:
undefined
,
location
:
undefined
,
isUploading
Resource
:
false
,
isUploading
Attachment
:
false
,
isRequesting
:
false
,
isRequesting
:
false
,
isComposing
:
false
,
isComposing
:
false
,
isDraggingFile
:
false
,
isDraggingFile
:
false
,
...
@@ -203,7 +203,7 @@ const MemoEditor = observer((props: Props) => {
...
@@ -203,7 +203,7 @@ const MemoEditor = observer((props: Props) => {
setState
((
state
)
=>
{
setState
((
state
)
=>
{
return
{
return
{
...
state
,
...
state
,
isUploading
Resource
:
true
,
isUploading
Attachment
:
true
,
};
};
});
});
...
@@ -223,7 +223,7 @@ const MemoEditor = observer((props: Props) => {
...
@@ -223,7 +223,7 @@ const MemoEditor = observer((props: Props) => {
setState
((
state
)
=>
{
setState
((
state
)
=>
{
return
{
return
{
...
state
,
...
state
,
isUploading
Resource
:
false
,
isUploading
Attachment
:
false
,
};
};
});
});
return
attachment
;
return
attachment
;
...
@@ -233,7 +233,7 @@ const MemoEditor = observer((props: Props) => {
...
@@ -233,7 +233,7 @@ const MemoEditor = observer((props: Props) => {
setState
((
state
)
=>
{
setState
((
state
)
=>
{
return
{
return
{
...
state
,
...
state
,
isUploading
Resource
:
false
,
isUploading
Attachment
:
false
,
};
};
});
});
}
}
...
@@ -456,7 +456,7 @@ const MemoEditor = observer((props: Props) => {
...
@@ -456,7 +456,7 @@ const MemoEditor = observer((props: Props) => {
[
i18n
.
language
],
[
i18n
.
language
],
);
);
const
allowSave
=
(
hasContent
||
state
.
attachmentList
.
length
>
0
)
&&
!
state
.
isUploading
Resource
&&
!
state
.
isRequesting
;
const
allowSave
=
(
hasContent
||
state
.
attachmentList
.
length
>
0
)
&&
!
state
.
isUploading
Attachment
&&
!
state
.
isRequesting
;
return
(
return
(
<
MemoEditorContext
.
Provider
<
MemoEditorContext
.
Provider
...
@@ -502,7 +502,7 @@ const MemoEditor = observer((props: Props) => {
...
@@ -502,7 +502,7 @@ const MemoEditor = observer((props: Props) => {
<
div
className=
"flex flex-row justify-start items-center opacity-80 dark:opacity-60 space-x-2"
>
<
div
className=
"flex flex-row justify-start items-center opacity-80 dark:opacity-60 space-x-2"
>
<
TagSelector
editorRef=
{
editorRef
}
/>
<
TagSelector
editorRef=
{
editorRef
}
/>
<
MarkdownMenu
editorRef=
{
editorRef
}
/>
<
MarkdownMenu
editorRef=
{
editorRef
}
/>
<
Upload
ResourceButton
isUploadingResource=
{
state
.
isUploadingResource
}
/>
<
Upload
AttachmentButton
isUploading=
{
state
.
isUploadingAttachment
}
/>
<
AddMemoRelationPopover
editorRef=
{
editorRef
}
/>
<
AddMemoRelationPopover
editorRef=
{
editorRef
}
/>
<
LocationSelector
<
LocationSelector
location=
{
state
.
location
}
location=
{
state
.
location
}
...
...
web/src/components/MemoResource.tsx
View file @
a4920d46
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
;
web/src/pages/Attachments.tsx
View file @
a4920d46
...
@@ -5,9 +5,9 @@ import { includes } from "lodash-es";
...
@@ -5,9 +5,9 @@ import { includes } from "lodash-es";
import
{
PaperclipIcon
,
SearchIcon
,
TrashIcon
}
from
"lucide-react"
;
import
{
PaperclipIcon
,
SearchIcon
,
TrashIcon
}
from
"lucide-react"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
observer
}
from
"mobx-react-lite"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
{
useEffect
,
useState
}
from
"react"
;
import
AttachmentIcon
from
"@/components/AttachmentIcon"
;
import
Empty
from
"@/components/Empty"
;
import
Empty
from
"@/components/Empty"
;
import
MobileHeader
from
"@/components/MobileHeader"
;
import
MobileHeader
from
"@/components/MobileHeader"
;
import
ResourceIcon
from
"@/components/ResourceIcon"
;
import
{
attachmentServiceClient
}
from
"@/grpcweb"
;
import
{
attachmentServiceClient
}
from
"@/grpcweb"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
useLoading
from
"@/hooks/useLoading"
;
import
useResponsiveWidth
from
"@/hooks/useResponsiveWidth"
;
import
useResponsiveWidth
from
"@/hooks/useResponsiveWidth"
;
...
@@ -112,7 +112,7 @@ const Attachments = observer(() => {
...
@@ -112,7 +112,7 @@ const Attachments = observer(() => {
return
(
return
(
<
div
key=
{
attachment
.
name
}
className=
"w-24 sm:w-32 h-auto flex flex-col justify-start items-start"
>
<
div
key=
{
attachment
.
name
}
className=
"w-24 sm:w-32 h-auto flex flex-col justify-start items-start"
>
<
div
className=
"w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-zinc-200 dark:border-zinc-900 overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80"
>
<
div
className=
"w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-zinc-200 dark:border-zinc-900 overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80"
>
<
ResourceIcon
resource
=
{
attachment
}
strokeWidth=
{
0.5
}
/>
<
AttachmentIcon
attachment
=
{
attachment
}
strokeWidth=
{
0.5
}
/>
</
div
>
</
div
>
<
div
className=
"w-full max-w-full flex flex-row justify-between items-center mt-1 px-1"
>
<
div
className=
"w-full max-w-full flex flex-row justify-between items-center mt-1 px-1"
>
<
p
className=
"text-xs shrink text-gray-400 truncate"
>
{
attachment
.
filename
}
</
p
>
<
p
className=
"text-xs shrink text-gray-400 truncate"
>
{
attachment
.
filename
}
</
p
>
...
@@ -144,7 +144,7 @@ const Attachments = observer(() => {
...
@@ -144,7 +144,7 @@ const Attachments = observer(() => {
return
(
return
(
<
div
key=
{
attachment
.
name
}
className=
"w-24 sm:w-32 h-auto flex flex-col justify-start items-start"
>
<
div
key=
{
attachment
.
name
}
className=
"w-24 sm:w-32 h-auto flex flex-col justify-start items-start"
>
<
div
className=
"w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-zinc-200 dark:border-zinc-900 overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80"
>
<
div
className=
"w-24 h-24 flex justify-center items-center sm:w-32 sm:h-32 border border-zinc-200 dark:border-zinc-900 overflow-clip rounded-xl cursor-pointer hover:shadow hover:opacity-80"
>
<
ResourceIcon
resource
=
{
attachment
}
strokeWidth=
{
0.5
}
/>
<
AttachmentIcon
attachment
=
{
attachment
}
strokeWidth=
{
0.5
}
/>
</
div
>
</
div
>
<
div
className=
"w-full max-w-full flex flex-row justify-between items-center mt-1 px-1"
>
<
div
className=
"w-full max-w-full flex flex-row justify-between items-center mt-1 px-1"
>
<
p
className=
"text-xs shrink text-gray-400 truncate"
>
{
attachment
.
filename
}
</
p
>
<
p
className=
"text-xs shrink text-gray-400 truncate"
>
{
attachment
.
filename
}
</
p
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment