Commit bb5809ca authored by Steven's avatar Steven

refactor: attachment service

parent 174b1a03
syntax = "proto3";
package memos.api.v1;
import "google/api/annotations.proto";
import "google/api/client.proto";
import "google/api/field_behavior.proto";
import "google/api/httpbody.proto";
import "google/api/resource.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto";
option go_package = "gen/api/v1";
service AttachmentService {
// CreateAttachment creates a new attachment.
rpc CreateAttachment(CreateAttachmentRequest) returns (Attachment) {
option (google.api.http) = {
post: "/api/v1/attachments"
body: "attachment"
};
option (google.api.method_signature) = "attachment";
}
// ListAttachments lists all attachments.
rpc ListAttachments(ListAttachmentsRequest) returns (ListAttachmentsResponse) {
option (google.api.http) = {get: "/api/v1/attachments"};
}
// GetAttachment returns a attachment by name.
rpc GetAttachment(GetAttachmentRequest) returns (Attachment) {
option (google.api.http) = {get: "/api/v1/{name=attachments/*}"};
option (google.api.method_signature) = "name";
}
// GetAttachmentBinary returns a attachment binary by name.
rpc GetAttachmentBinary(GetAttachmentBinaryRequest) returns (google.api.HttpBody) {
option (google.api.http) = {get: "/file/{name=attachments/*}/{filename}"};
option (google.api.method_signature) = "name,filename";
}
// UpdateAttachment updates a attachment.
rpc UpdateAttachment(UpdateAttachmentRequest) returns (Attachment) {
option (google.api.http) = {
patch: "/api/v1/{attachment.name=attachments/*}"
body: "attachment"
};
option (google.api.method_signature) = "attachment,update_mask";
}
// DeleteAttachment deletes a attachment by name.
rpc DeleteAttachment(DeleteAttachmentRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {delete: "/api/v1/{name=attachments/*}"};
option (google.api.method_signature) = "name";
}
}
message Attachment {
option (google.api.resource) = {
type: "memos.api.v1/Attachment"
pattern: "attachments/{attachment}"
singular: "attachment"
plural: "attachments"
};
reserved 2;
// The name of the attachment.
// Format: attachments/{attachment}
string name = 1 [(google.api.field_behavior) = IDENTIFIER];
// Output only. The creation timestamp.
google.protobuf.Timestamp create_time = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
// The filename of the attachment.
string filename = 4 [(google.api.field_behavior) = REQUIRED];
// Input only. The content of the attachment.
bytes content = 5 [(google.api.field_behavior) = INPUT_ONLY];
// Optional. The external link of the attachment.
string external_link = 6 [(google.api.field_behavior) = OPTIONAL];
// The MIME type of the attachment.
string type = 7 [(google.api.field_behavior) = REQUIRED];
// Output only. The size of the attachment in bytes.
int64 size = 8 [(google.api.field_behavior) = OUTPUT_ONLY];
// Optional. The related memo. Refer to `Memo.name`.
// Format: memos/{memo}
optional string memo = 9 [(google.api.field_behavior) = OPTIONAL];
}
message CreateAttachmentRequest {
// Required. The attachment to create.
Attachment attachment = 1 [(google.api.field_behavior) = REQUIRED];
// Optional. The attachment ID to use for this attachment.
// If empty, a unique ID will be generated.
string attachment_id = 2 [(google.api.field_behavior) = OPTIONAL];
}
message ListAttachmentsRequest {
// Optional. The maximum number of attachments to return.
// The service may return fewer than this value.
// If unspecified, at most 50 attachments will be returned.
// The maximum value is 1000; values above 1000 will be coerced to 1000.
int32 page_size = 1 [(google.api.field_behavior) = OPTIONAL];
// Optional. A page token, received from a previous `ListAttachments` call.
// Provide this to retrieve the subsequent page.
string page_token = 2 [(google.api.field_behavior) = OPTIONAL];
// Optional. Filter to apply to the list results.
// Example: "type=image/png" or "filename:*.jpg"
// Supported operators: =, !=, <, <=, >, >=, :
// Supported fields: filename, type, size, create_time, memo
string filter = 3 [(google.api.field_behavior) = OPTIONAL];
// Optional. The order to sort results by.
// Example: "create_time desc" or "filename asc"
string order_by = 4 [(google.api.field_behavior) = OPTIONAL];
}
message ListAttachmentsResponse {
// The list of attachments.
repeated Attachment attachments = 1;
// A token that can be sent as `page_token` to retrieve the next page.
// If this field is omitted, there are no subsequent pages.
string next_page_token = 2;
// The total count of attachments (may be approximate).
int32 total_size = 3;
}
message GetAttachmentRequest {
// Required. The attachment name of the attachment to retrieve.
// Format: attachments/{attachment}
string name = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/Attachment"}
];
}
message GetAttachmentBinaryRequest {
// Required. The attachment name of the attachment.
// Format: attachments/{attachment}
string name = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/Attachment"}
];
// The filename of the attachment. Mainly used for downloading.
string filename = 2 [(google.api.field_behavior) = REQUIRED];
// Optional. A flag indicating if the thumbnail version of the attachment should be returned.
bool thumbnail = 3 [(google.api.field_behavior) = OPTIONAL];
}
message UpdateAttachmentRequest {
// Required. The attachment which replaces the attachment on the server.
Attachment attachment = 1 [(google.api.field_behavior) = REQUIRED];
// Required. The list of fields to update.
google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED];
}
message DeleteAttachmentRequest {
// Required. The attachment name of the attachment to delete.
// Format: attachments/{attachment}
string name = 1 [
(google.api.field_behavior) = REQUIRED,
(google.api.resource_reference) = {type: "memos.api.v1/Attachment"}
];
}
...@@ -2,10 +2,10 @@ syntax = "proto3"; ...@@ -2,10 +2,10 @@ syntax = "proto3";
package memos.api.v1; package memos.api.v1;
import "api/v1/attachment_service.proto";
import "api/v1/common.proto"; import "api/v1/common.proto";
import "api/v1/markdown_service.proto"; import "api/v1/markdown_service.proto";
import "api/v1/reaction_service.proto"; import "api/v1/reaction_service.proto";
import "api/v1/resource_service.proto";
import "google/api/annotations.proto"; import "google/api/annotations.proto";
import "google/api/client.proto"; import "google/api/client.proto";
import "google/api/field_behavior.proto"; import "google/api/field_behavior.proto";
...@@ -59,17 +59,17 @@ service MemoService { ...@@ -59,17 +59,17 @@ service MemoService {
rpc DeleteMemoTag(DeleteMemoTagRequest) returns (google.protobuf.Empty) { rpc DeleteMemoTag(DeleteMemoTagRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {delete: "/api/v1/{parent=memos/*}/tags/{tag}"}; option (google.api.http) = {delete: "/api/v1/{parent=memos/*}/tags/{tag}"};
} }
// SetMemoResources sets resources for a memo. // SetMemoAttachments sets attachments for a memo.
rpc SetMemoResources(SetMemoResourcesRequest) returns (google.protobuf.Empty) { rpc SetMemoAttachments(SetMemoAttachmentsRequest) returns (google.protobuf.Empty) {
option (google.api.http) = { option (google.api.http) = {
patch: "/api/v1/{name=memos/*}/resources" patch: "/api/v1/{name=memos/*}/attachments"
body: "*" body: "*"
}; };
option (google.api.method_signature) = "name"; option (google.api.method_signature) = "name";
} }
// ListMemoResources lists resources for a memo. // ListMemoAttachments lists attachments for a memo.
rpc ListMemoResources(ListMemoResourcesRequest) returns (ListMemoResourcesResponse) { rpc ListMemoAttachments(ListMemoAttachmentsRequest) returns (ListMemoAttachmentsResponse) {
option (google.api.http) = {get: "/api/v1/{name=memos/*}/resources"}; option (google.api.http) = {get: "/api/v1/{name=memos/*}/attachments"};
option (google.api.method_signature) = "name"; option (google.api.method_signature) = "name";
} }
// SetMemoRelations sets relations for a memo. // SetMemoRelations sets relations for a memo.
...@@ -157,7 +157,7 @@ message Memo { ...@@ -157,7 +157,7 @@ message Memo {
bool pinned = 12; bool pinned = 12;
repeated Resource resources = 14; repeated Attachment attachments = 14;
repeated MemoRelation relations = 15; repeated MemoRelation relations = 15;
...@@ -269,20 +269,20 @@ message DeleteMemoTagRequest { ...@@ -269,20 +269,20 @@ message DeleteMemoTagRequest {
bool delete_related_memos = 3; bool delete_related_memos = 3;
} }
message SetMemoResourcesRequest { message SetMemoAttachmentsRequest {
// The name of the memo. // The name of the memo.
string name = 1; string name = 1;
repeated Resource resources = 2; repeated Attachment attachments = 2;
} }
message ListMemoResourcesRequest { message ListMemoAttachmentsRequest {
// The name of the memo. // The name of the memo.
string name = 1; string name = 1;
} }
message ListMemoResourcesResponse { message ListMemoAttachmentsResponse {
repeated Resource resources = 1; repeated Attachment attachments = 1;
} }
message MemoRelation { message MemoRelation {
......
syntax = "proto3";
package memos.api.v1;
import "google/api/annotations.proto";
import "google/api/client.proto";
import "google/api/field_behavior.proto";
import "google/api/httpbody.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto";
option go_package = "gen/api/v1";
service ResourceService {
// CreateResource creates a new resource.
rpc CreateResource(CreateResourceRequest) returns (Resource) {
option (google.api.http) = {
post: "/api/v1/resources"
body: "resource"
};
}
// ListResources lists all resources.
rpc ListResources(ListResourcesRequest) returns (ListResourcesResponse) {
option (google.api.http) = {get: "/api/v1/resources"};
}
// GetResource returns a resource by name.
rpc GetResource(GetResourceRequest) returns (Resource) {
option (google.api.http) = {get: "/api/v1/{name=resources/*}"};
option (google.api.method_signature) = "name";
}
// GetResourceBinary returns a resource binary by name.
rpc GetResourceBinary(GetResourceBinaryRequest) returns (google.api.HttpBody) {
option (google.api.http) = {get: "/file/{name=resources/*}/{filename}"};
option (google.api.method_signature) = "name,filename";
}
// UpdateResource updates a resource.
rpc UpdateResource(UpdateResourceRequest) returns (Resource) {
option (google.api.http) = {
patch: "/api/v1/{resource.name=resources/*}"
body: "resource"
};
option (google.api.method_signature) = "resource,update_mask";
}
// DeleteResource deletes a resource by name.
rpc DeleteResource(DeleteResourceRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {delete: "/api/v1/{name=resources/*}"};
option (google.api.method_signature) = "name";
}
}
message Resource {
reserved 2;
// The name of the resource.
// Format: resources/{resource}, resource is the user defined if or uuid.
string name = 1 [
(google.api.field_behavior) = OUTPUT_ONLY,
(google.api.field_behavior) = IDENTIFIER
];
google.protobuf.Timestamp create_time = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
string filename = 4;
bytes content = 5 [(google.api.field_behavior) = INPUT_ONLY];
string external_link = 6;
string type = 7;
int64 size = 8;
// The related memo. Refer to `Memo.name`.
optional string memo = 9;
}
message CreateResourceRequest {
Resource resource = 1;
}
message ListResourcesRequest {}
message ListResourcesResponse {
repeated Resource resources = 1;
}
message GetResourceRequest {
// The name of the resource.
string name = 1;
}
message GetResourceBinaryRequest {
// The name of the resource.
string name = 1;
// The filename of the resource. Mainly used for downloading.
string filename = 2;
// A flag indicating if the thumbnail version of the resource should be returned
bool thumbnail = 3;
}
message UpdateResourceRequest {
Resource resource = 1;
google.protobuf.FieldMask update_mask = 2;
}
message DeleteResourceRequest {
// The name of the resource.
string name = 1;
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -27,8 +27,8 @@ const ( ...@@ -27,8 +27,8 @@ const (
MemoService_DeleteMemo_FullMethodName = "/memos.api.v1.MemoService/DeleteMemo" MemoService_DeleteMemo_FullMethodName = "/memos.api.v1.MemoService/DeleteMemo"
MemoService_RenameMemoTag_FullMethodName = "/memos.api.v1.MemoService/RenameMemoTag" MemoService_RenameMemoTag_FullMethodName = "/memos.api.v1.MemoService/RenameMemoTag"
MemoService_DeleteMemoTag_FullMethodName = "/memos.api.v1.MemoService/DeleteMemoTag" MemoService_DeleteMemoTag_FullMethodName = "/memos.api.v1.MemoService/DeleteMemoTag"
MemoService_SetMemoResources_FullMethodName = "/memos.api.v1.MemoService/SetMemoResources" MemoService_SetMemoAttachments_FullMethodName = "/memos.api.v1.MemoService/SetMemoAttachments"
MemoService_ListMemoResources_FullMethodName = "/memos.api.v1.MemoService/ListMemoResources" MemoService_ListMemoAttachments_FullMethodName = "/memos.api.v1.MemoService/ListMemoAttachments"
MemoService_SetMemoRelations_FullMethodName = "/memos.api.v1.MemoService/SetMemoRelations" MemoService_SetMemoRelations_FullMethodName = "/memos.api.v1.MemoService/SetMemoRelations"
MemoService_ListMemoRelations_FullMethodName = "/memos.api.v1.MemoService/ListMemoRelations" MemoService_ListMemoRelations_FullMethodName = "/memos.api.v1.MemoService/ListMemoRelations"
MemoService_CreateMemoComment_FullMethodName = "/memos.api.v1.MemoService/CreateMemoComment" MemoService_CreateMemoComment_FullMethodName = "/memos.api.v1.MemoService/CreateMemoComment"
...@@ -56,10 +56,10 @@ type MemoServiceClient interface { ...@@ -56,10 +56,10 @@ type MemoServiceClient interface {
RenameMemoTag(ctx context.Context, in *RenameMemoTagRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) RenameMemoTag(ctx context.Context, in *RenameMemoTagRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// DeleteMemoTag deletes a tag for a memo. // DeleteMemoTag deletes a tag for a memo.
DeleteMemoTag(ctx context.Context, in *DeleteMemoTagRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) DeleteMemoTag(ctx context.Context, in *DeleteMemoTagRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// SetMemoResources sets resources for a memo. // SetMemoAttachments sets attachments for a memo.
SetMemoResources(ctx context.Context, in *SetMemoResourcesRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) SetMemoAttachments(ctx context.Context, in *SetMemoAttachmentsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// ListMemoResources lists resources for a memo. // ListMemoAttachments lists attachments for a memo.
ListMemoResources(ctx context.Context, in *ListMemoResourcesRequest, opts ...grpc.CallOption) (*ListMemoResourcesResponse, error) ListMemoAttachments(ctx context.Context, in *ListMemoAttachmentsRequest, opts ...grpc.CallOption) (*ListMemoAttachmentsResponse, error)
// SetMemoRelations sets relations for a memo. // SetMemoRelations sets relations for a memo.
SetMemoRelations(ctx context.Context, in *SetMemoRelationsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) SetMemoRelations(ctx context.Context, in *SetMemoRelationsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
// ListMemoRelations lists relations for a memo. // ListMemoRelations lists relations for a memo.
...@@ -154,20 +154,20 @@ func (c *memoServiceClient) DeleteMemoTag(ctx context.Context, in *DeleteMemoTag ...@@ -154,20 +154,20 @@ func (c *memoServiceClient) DeleteMemoTag(ctx context.Context, in *DeleteMemoTag
return out, nil return out, nil
} }
func (c *memoServiceClient) SetMemoResources(ctx context.Context, in *SetMemoResourcesRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { func (c *memoServiceClient) SetMemoAttachments(ctx context.Context, in *SetMemoAttachmentsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(emptypb.Empty) out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, MemoService_SetMemoResources_FullMethodName, in, out, cOpts...) err := c.cc.Invoke(ctx, MemoService_SetMemoAttachments_FullMethodName, in, out, cOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return out, nil return out, nil
} }
func (c *memoServiceClient) ListMemoResources(ctx context.Context, in *ListMemoResourcesRequest, opts ...grpc.CallOption) (*ListMemoResourcesResponse, error) { func (c *memoServiceClient) ListMemoAttachments(ctx context.Context, in *ListMemoAttachmentsRequest, opts ...grpc.CallOption) (*ListMemoAttachmentsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListMemoResourcesResponse) out := new(ListMemoAttachmentsResponse)
err := c.cc.Invoke(ctx, MemoService_ListMemoResources_FullMethodName, in, out, cOpts...) err := c.cc.Invoke(ctx, MemoService_ListMemoAttachments_FullMethodName, in, out, cOpts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -262,10 +262,10 @@ type MemoServiceServer interface { ...@@ -262,10 +262,10 @@ type MemoServiceServer interface {
RenameMemoTag(context.Context, *RenameMemoTagRequest) (*emptypb.Empty, error) RenameMemoTag(context.Context, *RenameMemoTagRequest) (*emptypb.Empty, error)
// DeleteMemoTag deletes a tag for a memo. // DeleteMemoTag deletes a tag for a memo.
DeleteMemoTag(context.Context, *DeleteMemoTagRequest) (*emptypb.Empty, error) DeleteMemoTag(context.Context, *DeleteMemoTagRequest) (*emptypb.Empty, error)
// SetMemoResources sets resources for a memo. // SetMemoAttachments sets attachments for a memo.
SetMemoResources(context.Context, *SetMemoResourcesRequest) (*emptypb.Empty, error) SetMemoAttachments(context.Context, *SetMemoAttachmentsRequest) (*emptypb.Empty, error)
// ListMemoResources lists resources for a memo. // ListMemoAttachments lists attachments for a memo.
ListMemoResources(context.Context, *ListMemoResourcesRequest) (*ListMemoResourcesResponse, error) ListMemoAttachments(context.Context, *ListMemoAttachmentsRequest) (*ListMemoAttachmentsResponse, error)
// SetMemoRelations sets relations for a memo. // SetMemoRelations sets relations for a memo.
SetMemoRelations(context.Context, *SetMemoRelationsRequest) (*emptypb.Empty, error) SetMemoRelations(context.Context, *SetMemoRelationsRequest) (*emptypb.Empty, error)
// ListMemoRelations lists relations for a memo. // ListMemoRelations lists relations for a memo.
...@@ -311,11 +311,11 @@ func (UnimplementedMemoServiceServer) RenameMemoTag(context.Context, *RenameMemo ...@@ -311,11 +311,11 @@ func (UnimplementedMemoServiceServer) RenameMemoTag(context.Context, *RenameMemo
func (UnimplementedMemoServiceServer) DeleteMemoTag(context.Context, *DeleteMemoTagRequest) (*emptypb.Empty, error) { func (UnimplementedMemoServiceServer) DeleteMemoTag(context.Context, *DeleteMemoTagRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteMemoTag not implemented") return nil, status.Errorf(codes.Unimplemented, "method DeleteMemoTag not implemented")
} }
func (UnimplementedMemoServiceServer) SetMemoResources(context.Context, *SetMemoResourcesRequest) (*emptypb.Empty, error) { func (UnimplementedMemoServiceServer) SetMemoAttachments(context.Context, *SetMemoAttachmentsRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetMemoResources not implemented") return nil, status.Errorf(codes.Unimplemented, "method SetMemoAttachments not implemented")
} }
func (UnimplementedMemoServiceServer) ListMemoResources(context.Context, *ListMemoResourcesRequest) (*ListMemoResourcesResponse, error) { func (UnimplementedMemoServiceServer) ListMemoAttachments(context.Context, *ListMemoAttachmentsRequest) (*ListMemoAttachmentsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListMemoResources not implemented") return nil, status.Errorf(codes.Unimplemented, "method ListMemoAttachments not implemented")
} }
func (UnimplementedMemoServiceServer) SetMemoRelations(context.Context, *SetMemoRelationsRequest) (*emptypb.Empty, error) { func (UnimplementedMemoServiceServer) SetMemoRelations(context.Context, *SetMemoRelationsRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetMemoRelations not implemented") return nil, status.Errorf(codes.Unimplemented, "method SetMemoRelations not implemented")
...@@ -485,38 +485,38 @@ func _MemoService_DeleteMemoTag_Handler(srv interface{}, ctx context.Context, de ...@@ -485,38 +485,38 @@ func _MemoService_DeleteMemoTag_Handler(srv interface{}, ctx context.Context, de
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _MemoService_SetMemoResources_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _MemoService_SetMemoAttachments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetMemoResourcesRequest) in := new(SetMemoAttachmentsRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
return nil, err return nil, err
} }
if interceptor == nil { if interceptor == nil {
return srv.(MemoServiceServer).SetMemoResources(ctx, in) return srv.(MemoServiceServer).SetMemoAttachments(ctx, in)
} }
info := &grpc.UnaryServerInfo{ info := &grpc.UnaryServerInfo{
Server: srv, Server: srv,
FullMethod: MemoService_SetMemoResources_FullMethodName, FullMethod: MemoService_SetMemoAttachments_FullMethodName,
} }
handler := func(ctx context.Context, req interface{}) (interface{}, error) { handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MemoServiceServer).SetMemoResources(ctx, req.(*SetMemoResourcesRequest)) return srv.(MemoServiceServer).SetMemoAttachments(ctx, req.(*SetMemoAttachmentsRequest))
} }
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _MemoService_ListMemoResources_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _MemoService_ListMemoAttachments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListMemoResourcesRequest) in := new(ListMemoAttachmentsRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
return nil, err return nil, err
} }
if interceptor == nil { if interceptor == nil {
return srv.(MemoServiceServer).ListMemoResources(ctx, in) return srv.(MemoServiceServer).ListMemoAttachments(ctx, in)
} }
info := &grpc.UnaryServerInfo{ info := &grpc.UnaryServerInfo{
Server: srv, Server: srv,
FullMethod: MemoService_ListMemoResources_FullMethodName, FullMethod: MemoService_ListMemoAttachments_FullMethodName,
} }
handler := func(ctx context.Context, req interface{}) (interface{}, error) { handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MemoServiceServer).ListMemoResources(ctx, req.(*ListMemoResourcesRequest)) return srv.(MemoServiceServer).ListMemoAttachments(ctx, req.(*ListMemoAttachmentsRequest))
} }
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
...@@ -683,12 +683,12 @@ var MemoService_ServiceDesc = grpc.ServiceDesc{ ...@@ -683,12 +683,12 @@ var MemoService_ServiceDesc = grpc.ServiceDesc{
Handler: _MemoService_DeleteMemoTag_Handler, Handler: _MemoService_DeleteMemoTag_Handler,
}, },
{ {
MethodName: "SetMemoResources", MethodName: "SetMemoAttachments",
Handler: _MemoService_SetMemoResources_Handler, Handler: _MemoService_SetMemoAttachments_Handler,
}, },
{ {
MethodName: "ListMemoResources", MethodName: "ListMemoAttachments",
Handler: _MemoService_ListMemoResources_Handler, Handler: _MemoService_ListMemoAttachments_Handler,
}, },
{ {
MethodName: "SetMemoRelations", MethodName: "SetMemoRelations",
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -13,7 +13,7 @@ import ( ...@@ -13,7 +13,7 @@ import (
"github.com/usememos/memos/store" "github.com/usememos/memos/store"
) )
func (s *APIV1Service) SetMemoResources(ctx context.Context, request *v1pb.SetMemoResourcesRequest) (*emptypb.Empty, error) { func (s *APIV1Service) SetMemoAttachments(ctx context.Context, request *v1pb.SetMemoAttachmentsRequest) (*emptypb.Empty, error) {
memoUID, err := ExtractMemoUIDFromName(request.Name) memoUID, err := ExtractMemoUIDFromName(request.Name)
if err != nil { if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err) return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
...@@ -32,10 +32,10 @@ func (s *APIV1Service) SetMemoResources(ctx context.Context, request *v1pb.SetMe ...@@ -32,10 +32,10 @@ func (s *APIV1Service) SetMemoResources(ctx context.Context, request *v1pb.SetMe
// Delete resources that are not in the request. // Delete resources that are not in the request.
for _, resource := range resources { for _, resource := range resources {
found := false found := false
for _, requestResource := range request.Resources { for _, requestResource := range request.Attachments {
requestResourceUID, err := ExtractResourceUIDFromName(requestResource.Name) requestResourceUID, err := ExtractAttachmentUIDFromName(requestResource.Name)
if err != nil { if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid resource name: %v", err) return nil, status.Errorf(codes.InvalidArgument, "invalid attachment name: %v", err)
} }
if resource.UID == requestResourceUID { if resource.UID == requestResourceUID {
found = true found = true
...@@ -52,12 +52,12 @@ func (s *APIV1Service) SetMemoResources(ctx context.Context, request *v1pb.SetMe ...@@ -52,12 +52,12 @@ func (s *APIV1Service) SetMemoResources(ctx context.Context, request *v1pb.SetMe
} }
} }
slices.Reverse(request.Resources) slices.Reverse(request.Attachments)
// Update resources' memo_id in the request. // Update resources' memo_id in the request.
for index, resource := range request.Resources { for index, resource := range request.Attachments {
resourceUID, err := ExtractResourceUIDFromName(resource.Name) resourceUID, err := ExtractAttachmentUIDFromName(resource.Name)
if err != nil { if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid resource name: %v", err) return nil, status.Errorf(codes.InvalidArgument, "invalid attachment name: %v", err)
} }
tempResource, err := s.Store.GetResource(ctx, &store.FindResource{UID: &resourceUID}) tempResource, err := s.Store.GetResource(ctx, &store.FindResource{UID: &resourceUID})
if err != nil { if err != nil {
...@@ -76,7 +76,7 @@ func (s *APIV1Service) SetMemoResources(ctx context.Context, request *v1pb.SetMe ...@@ -76,7 +76,7 @@ func (s *APIV1Service) SetMemoResources(ctx context.Context, request *v1pb.SetMe
return &emptypb.Empty{}, nil return &emptypb.Empty{}, nil
} }
func (s *APIV1Service) ListMemoResources(ctx context.Context, request *v1pb.ListMemoResourcesRequest) (*v1pb.ListMemoResourcesResponse, error) { func (s *APIV1Service) ListMemoAttachments(ctx context.Context, request *v1pb.ListMemoAttachmentsRequest) (*v1pb.ListMemoAttachmentsResponse, error) {
memoUID, err := ExtractMemoUIDFromName(request.Name) memoUID, err := ExtractMemoUIDFromName(request.Name)
if err != nil { if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err) return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err)
...@@ -92,11 +92,11 @@ func (s *APIV1Service) ListMemoResources(ctx context.Context, request *v1pb.List ...@@ -92,11 +92,11 @@ func (s *APIV1Service) ListMemoResources(ctx context.Context, request *v1pb.List
return nil, status.Errorf(codes.Internal, "failed to list resources: %v", err) return nil, status.Errorf(codes.Internal, "failed to list resources: %v", err)
} }
response := &v1pb.ListMemoResourcesResponse{ response := &v1pb.ListMemoAttachmentsResponse{
Resources: []*v1pb.Resource{}, Attachments: []*v1pb.Attachment{},
} }
for _, resource := range resources { for _, resource := range resources {
response.Resources = append(response.Resources, s.convertResourceFromStore(ctx, resource)) response.Attachments = append(response.Attachments, s.convertAttachmentFromStore(ctx, resource))
} }
return response, nil return response, nil
} }
...@@ -63,13 +63,13 @@ func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoR ...@@ -63,13 +63,13 @@ func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoR
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(request.Memo.Resources) > 0 { if len(request.Memo.Attachments) > 0 {
_, err := s.SetMemoResources(ctx, &v1pb.SetMemoResourcesRequest{ _, err := s.SetMemoAttachments(ctx, &v1pb.SetMemoAttachmentsRequest{
Name: fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID), Name: fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID),
Resources: request.Memo.Resources, Attachments: request.Memo.Attachments,
}) })
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to set memo resources") return nil, errors.Wrap(err, "failed to set memo attachments")
} }
} }
if len(request.Memo.Relations) > 0 { if len(request.Memo.Relations) > 0 {
...@@ -318,13 +318,13 @@ func (s *APIV1Service) UpdateMemo(ctx context.Context, request *v1pb.UpdateMemoR ...@@ -318,13 +318,13 @@ func (s *APIV1Service) UpdateMemo(ctx context.Context, request *v1pb.UpdateMemoR
payload := memo.Payload payload := memo.Payload
payload.Location = convertLocationToStore(request.Memo.Location) payload.Location = convertLocationToStore(request.Memo.Location)
update.Payload = payload update.Payload = payload
} else if path == "resources" { } else if path == "attachments" {
_, err := s.SetMemoResources(ctx, &v1pb.SetMemoResourcesRequest{ _, err := s.SetMemoAttachments(ctx, &v1pb.SetMemoAttachmentsRequest{
Name: request.Memo.Name, Name: request.Memo.Name,
Resources: request.Memo.Resources, Attachments: request.Memo.Attachments,
}) })
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to set memo resources") return nil, errors.Wrap(err, "failed to set memo attachments")
} }
} else if path == "relations" { } else if path == "relations" {
_, err := s.SetMemoRelations(ctx, &v1pb.SetMemoRelationsRequest{ _, err := s.SetMemoRelations(ctx, &v1pb.SetMemoRelationsRequest{
......
...@@ -61,11 +61,11 @@ func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Mem ...@@ -61,11 +61,11 @@ func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Mem
} }
memoMessage.Relations = listMemoRelationsResponse.Relations memoMessage.Relations = listMemoRelationsResponse.Relations
listMemoResourcesResponse, err := s.ListMemoResources(ctx, &v1pb.ListMemoResourcesRequest{Name: name}) listMemoAttachmentsResponse, err := s.ListMemoAttachments(ctx, &v1pb.ListMemoAttachmentsRequest{Name: name})
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to list memo resources") return nil, errors.Wrap(err, "failed to list memo attachments")
} }
memoMessage.Resources = listMemoResourcesResponse.Resources memoMessage.Attachments = listMemoAttachmentsResponse.Attachments
listMemoReactionsResponse, err := s.ListMemoReactions(ctx, &v1pb.ListMemoReactionsRequest{Name: name}) listMemoReactionsResponse, err := s.ListMemoReactions(ctx, &v1pb.ListMemoReactionsRequest{Name: name})
if err != nil { if err != nil {
......
...@@ -13,7 +13,7 @@ const ( ...@@ -13,7 +13,7 @@ const (
WorkspaceSettingNamePrefix = "workspace/settings/" WorkspaceSettingNamePrefix = "workspace/settings/"
UserNamePrefix = "users/" UserNamePrefix = "users/"
MemoNamePrefix = "memos/" MemoNamePrefix = "memos/"
ResourceNamePrefix = "resources/" AttachmentNamePrefix = "attachments/"
InboxNamePrefix = "inboxes/" InboxNamePrefix = "inboxes/"
IdentityProviderNamePrefix = "identityProviders/" IdentityProviderNamePrefix = "identityProviders/"
ActivityNamePrefix = "activities/" ActivityNamePrefix = "activities/"
...@@ -83,9 +83,9 @@ func ExtractMemoUIDFromName(name string) (string, error) { ...@@ -83,9 +83,9 @@ func ExtractMemoUIDFromName(name string) (string, error) {
return id, nil return id, nil
} }
// ExtractResourceUIDFromName returns the resource UID from a resource name. // ExtractAttachmentUIDFromName returns the attachment UID from a resource name.
func ExtractResourceUIDFromName(name string) (string, error) { func ExtractAttachmentUIDFromName(name string) (string, error) {
tokens, err := GetNameParentTokens(name, ResourceNamePrefix) tokens, err := GetNameParentTokens(name, AttachmentNamePrefix)
if err != nil { if err != nil {
return "", err return "", err
} }
......
...@@ -26,7 +26,7 @@ type APIV1Service struct { ...@@ -26,7 +26,7 @@ type APIV1Service struct {
v1pb.UnimplementedAuthServiceServer v1pb.UnimplementedAuthServiceServer
v1pb.UnimplementedUserServiceServer v1pb.UnimplementedUserServiceServer
v1pb.UnimplementedMemoServiceServer v1pb.UnimplementedMemoServiceServer
v1pb.UnimplementedResourceServiceServer v1pb.UnimplementedAttachmentServiceServer
v1pb.UnimplementedShortcutServiceServer v1pb.UnimplementedShortcutServiceServer
v1pb.UnimplementedInboxServiceServer v1pb.UnimplementedInboxServiceServer
v1pb.UnimplementedActivityServiceServer v1pb.UnimplementedActivityServiceServer
...@@ -54,7 +54,7 @@ func NewAPIV1Service(secret string, profile *profile.Profile, store *store.Store ...@@ -54,7 +54,7 @@ func NewAPIV1Service(secret string, profile *profile.Profile, store *store.Store
v1pb.RegisterAuthServiceServer(grpcServer, apiv1Service) v1pb.RegisterAuthServiceServer(grpcServer, apiv1Service)
v1pb.RegisterUserServiceServer(grpcServer, apiv1Service) v1pb.RegisterUserServiceServer(grpcServer, apiv1Service)
v1pb.RegisterMemoServiceServer(grpcServer, apiv1Service) v1pb.RegisterMemoServiceServer(grpcServer, apiv1Service)
v1pb.RegisterResourceServiceServer(grpcServer, apiv1Service) v1pb.RegisterAttachmentServiceServer(grpcServer, apiv1Service)
v1pb.RegisterShortcutServiceServer(grpcServer, apiv1Service) v1pb.RegisterShortcutServiceServer(grpcServer, apiv1Service)
v1pb.RegisterInboxServiceServer(grpcServer, apiv1Service) v1pb.RegisterInboxServiceServer(grpcServer, apiv1Service)
v1pb.RegisterActivityServiceServer(grpcServer, apiv1Service) v1pb.RegisterActivityServiceServer(grpcServer, apiv1Service)
...@@ -95,7 +95,7 @@ func (s *APIV1Service) RegisterGateway(ctx context.Context, echoServer *echo.Ech ...@@ -95,7 +95,7 @@ func (s *APIV1Service) RegisterGateway(ctx context.Context, echoServer *echo.Ech
if err := v1pb.RegisterMemoServiceHandler(ctx, gwMux, conn); err != nil { if err := v1pb.RegisterMemoServiceHandler(ctx, gwMux, conn); err != nil {
return err return err
} }
if err := v1pb.RegisterResourceServiceHandler(ctx, gwMux, conn); err != nil { if err := v1pb.RegisterAttachmentServiceHandler(ctx, gwMux, conn); err != nil {
return err return err
} }
if err := v1pb.RegisterShortcutServiceHandler(ctx, gwMux, conn); err != nil { if err := v1pb.RegisterShortcutServiceHandler(ctx, gwMux, conn); err != nil {
......
...@@ -136,7 +136,7 @@ func (s *RSSService) generateRSSFromMemoList(ctx context.Context, memoList []*st ...@@ -136,7 +136,7 @@ func (s *RSSService) generateRSSFromMemoList(ctx context.Context, memoList []*st
if resource.StorageType == storepb.ResourceStorageType_EXTERNAL || resource.StorageType == storepb.ResourceStorageType_S3 { if resource.StorageType == storepb.ResourceStorageType_EXTERNAL || resource.StorageType == storepb.ResourceStorageType_S3 {
enclosure.Url = resource.Reference enclosure.Url = resource.Reference
} else { } else {
enclosure.Url = fmt.Sprintf("%s/file/resources/%s/%s", baseURL, resource.UID, resource.Filename) enclosure.Url = fmt.Sprintf("%s/file/attachments/%s/%s", baseURL, resource.UID, resource.Filename)
} }
enclosure.Length = strconv.Itoa(int(resource.Size)) enclosure.Length = strconv.Itoa(int(resource.Size))
enclosure.Type = resource.Type enclosure.Type = resource.Type
......
...@@ -73,7 +73,7 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store ...@@ -73,7 +73,7 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store
return c.String(http.StatusOK, "Service ready.") return c.String(http.StatusOK, "Service ready.")
}) })
// Serve frontend resources. // Serve frontend static files.
frontend.NewFrontendService(profile, store).Serve(ctx, echoServer) frontend.NewFrontendService(profile, store).Serve(ctx, echoServer)
rootGroup := echoServer.Group("") rootGroup := echoServer.Group("")
...@@ -82,7 +82,7 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store ...@@ -82,7 +82,7 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store
rss.NewRSSService(s.Profile, s.Store).RegisterRoutes(rootGroup) rss.NewRSSService(s.Profile, s.Store).RegisterRoutes(rootGroup)
grpcServer := grpc.NewServer( grpcServer := grpc.NewServer(
// Override the maximum receiving message size to math.MaxInt32 for uploading large resources. // Override the maximum receiving message size to math.MaxInt32 for uploading large attachments.
grpc.MaxRecvMsgSize(math.MaxInt32), grpc.MaxRecvMsgSize(math.MaxInt32),
grpc.ChainUnaryInterceptor( grpc.ChainUnaryInterceptor(
apiv1.NewLoggerInterceptor().LoggerInterceptor, apiv1.NewLoggerInterceptor().LoggerInterceptor,
......
import { memo } from "react"; import { memo } from "react";
import { Resource } from "@/types/proto/api/v1/resource_service"; import { Attachment } from "@/types/proto/api/v1/attachment_service";
import { cn } from "@/utils"; import { cn } from "@/utils";
import { getResourceType, getResourceUrl } from "@/utils/resource"; import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment";
import MemoResource from "./MemoResource"; import MemoResource from "./MemoResource";
import showPreviewImageDialog from "./PreviewImageDialog"; import showPreviewImageDialog from "./PreviewImageDialog";
const MemoResourceListView = ({ resources = [] }: { resources: Resource[] }) => { const MemoAttachmentListView = ({ attachments = [] }: { attachments: Attachment[] }) => {
const mediaResources: Resource[] = []; const mediaAttachments: Attachment[] = [];
const otherResources: Resource[] = []; const otherAttachments: Attachment[] = [];
resources.forEach((resource) => { attachments.forEach((attachment) => {
const type = getResourceType(resource); const type = getAttachmentType(attachment);
if (type === "image/*" || type === "video/*") { if (type === "image/*" || type === "video/*") {
mediaResources.push(resource); mediaAttachments.push(attachment);
return; return;
} }
otherResources.push(resource); otherAttachments.push(attachment);
}); });
const handleImageClick = (imgUrl: string) => { const handleImageClick = (imgUrl: string) => {
const imgUrls = mediaResources const imgUrls = mediaAttachments
.filter((resource) => getResourceType(resource) === "image/*") .filter((attachment) => getAttachmentType(attachment) === "image/*")
.map((resource) => getResourceUrl(resource)); .map((attachment) => getAttachmentUrl(attachment));
const index = imgUrls.findIndex((url) => url === imgUrl); const index = imgUrls.findIndex((url) => url === imgUrl);
showPreviewImageDialog(imgUrls, index); showPreviewImageDialog(imgUrls, index);
}; };
const MediaCard = ({ resource, className }: { resource: Resource; className?: string }) => { const MediaCard = ({ attachment, className }: { attachment: Attachment; className?: string }) => {
const type = getResourceType(resource); const type = getAttachmentType(attachment);
const resourceUrl = getResourceUrl(resource); const attachmentUrl = getAttachmentUrl(attachment);
if (type === "image/*") { if (type === "image/*") {
return ( return (
...@@ -38,8 +38,8 @@ const MemoResourceListView = ({ resources = [] }: { resources: Resource[] }) => ...@@ -38,8 +38,8 @@ const MemoResourceListView = ({ resources = [] }: { resources: Resource[] }) =>
"cursor-pointer h-full w-auto rounded-lg border border-zinc-200 dark:border-zinc-800 object-contain hover:opacity-80", "cursor-pointer h-full w-auto rounded-lg border border-zinc-200 dark:border-zinc-800 object-contain hover:opacity-80",
className, className,
)} )}
src={resource.externalLink ? resourceUrl : resourceUrl + "?thumbnail=true"} src={attachment.externalLink ? attachmentUrl : attachmentUrl + "?thumbnail=true"}
onClick={() => handleImageClick(resourceUrl)} onClick={() => handleImageClick(attachmentUrl)}
decoding="async" decoding="async"
loading="lazy" loading="lazy"
/> />
...@@ -53,7 +53,7 @@ const MemoResourceListView = ({ resources = [] }: { resources: Resource[] }) => ...@@ -53,7 +53,7 @@ const MemoResourceListView = ({ resources = [] }: { resources: Resource[] }) =>
)} )}
preload="metadata" preload="metadata"
crossOrigin="anonymous" crossOrigin="anonymous"
src={resourceUrl} src={attachmentUrl}
controls controls
/> />
); );
...@@ -62,23 +62,23 @@ const MemoResourceListView = ({ resources = [] }: { resources: Resource[] }) => ...@@ -62,23 +62,23 @@ const MemoResourceListView = ({ resources = [] }: { resources: Resource[] }) =>
} }
}; };
const MediaList = ({ resources = [] }: { resources: Resource[] }) => { const MediaList = ({ attachments = [] }: { attachments: Attachment[] }) => {
const cards = resources.map((resource) => ( const cards = attachments.map((attachment) => (
<div key={resource.name} className="max-w-[70%] grow flex flex-col justify-start items-start shrink-0"> <div key={attachment.name} className="max-w-[70%] grow flex flex-col justify-start items-start shrink-0">
<MediaCard className="max-h-64 grow" resource={resource} /> <MediaCard className="max-h-64 grow" attachment={attachment} />
</div> </div>
)); ));
return <div className="w-full flex flex-row justify-start overflow-auto gap-2">{cards}</div>; return <div className="w-full flex flex-row justify-start overflow-auto gap-2">{cards}</div>;
}; };
const OtherList = ({ resources = [] }: { resources: Resource[] }) => { const OtherList = ({ attachments = [] }: { attachments: Attachment[] }) => {
if (resources.length === 0) return <></>; if (attachments.length === 0) return <></>;
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">
{otherResources.map((resource) => ( {otherAttachments.map((attachment) => (
<MemoResource key={resource.name} resource={resource} /> <MemoResource key={attachment.name} resource={attachment} />
))} ))}
</div> </div>
); );
...@@ -86,10 +86,10 @@ const MemoResourceListView = ({ resources = [] }: { resources: Resource[] }) => ...@@ -86,10 +86,10 @@ const MemoResourceListView = ({ resources = [] }: { resources: Resource[] }) =>
return ( return (
<> <>
{mediaResources.length > 0 && <MediaList resources={mediaResources} />} {mediaAttachments.length > 0 && <MediaList attachments={mediaAttachments} />}
<OtherList resources={otherResources} /> <OtherList attachments={otherAttachments} />
</> </>
); );
}; };
export default memo(MemoResourceListView); export default memo(MemoAttachmentListView);
...@@ -16,7 +16,7 @@ const EmbeddedContent = ({ resourceName, params }: Props) => { ...@@ -16,7 +16,7 @@ const EmbeddedContent = ({ resourceName, params }: Props) => {
const { resourceType, resourceId } = extractResourceTypeAndId(resourceName); const { resourceType, resourceId } = extractResourceTypeAndId(resourceName);
if (resourceType === "memos") { if (resourceType === "memos") {
return <EmbeddedMemo resourceId={resourceId} params={params} />; return <EmbeddedMemo resourceId={resourceId} params={params} />;
} else if (resourceType === "resources") { } else if (resourceType === "attachments") {
return <EmbeddedResource resourceId={resourceId} params={params} />; return <EmbeddedResource resourceId={resourceId} params={params} />;
} }
return <Error message={`Unknown resource: ${resourceName}`} />; return <Error message={`Unknown resource: ${resourceName}`} />;
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment