mirror of https://github.com/usememos/memos.git
refactor: migrate binary file serving from gRPC to dedicated HTTP fileserver
Migrates attachment and avatar binary serving from gRPC endpoints to a new dedicated HTTP fileserver package, fixing Safari video playback issues and improving architectural separation. Key changes: - Created server/router/fileserver package for all binary file serving - Removed GetAttachmentBinary and GetUserAvatar gRPC endpoints from proto - Implemented native HTTP handlers with full range request support - Added authentication support (session cookies + JWT) to fileserver - New avatar endpoint supports lookup by user ID or username - Eliminated duplicate auth constants (imports from api/v1) HTTP endpoints: - Attachments: /file/attachments/:uid/:filename (unchanged URL) - Avatars: /file/users/:identifier/avatar (new URL format) This fixes Safari video/audio playback by using http.ServeContent() which properly handles HTTP 206 Partial Content responses and range request headers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9ea27ee61f
commit
1cf047707b
|
|
@ -5,7 +5,6 @@ package memos.api.v1;
|
||||||
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";
|
||||||
import "google/api/httpbody.proto";
|
|
||||||
import "google/api/resource.proto";
|
import "google/api/resource.proto";
|
||||||
import "google/protobuf/empty.proto";
|
import "google/protobuf/empty.proto";
|
||||||
import "google/protobuf/field_mask.proto";
|
import "google/protobuf/field_mask.proto";
|
||||||
|
|
@ -31,11 +30,6 @@ service AttachmentService {
|
||||||
option (google.api.http) = {get: "/api/v1/{name=attachments/*}"};
|
option (google.api.http) = {get: "/api/v1/{name=attachments/*}"};
|
||||||
option (google.api.method_signature) = "name";
|
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,thumbnail";
|
|
||||||
}
|
|
||||||
// UpdateAttachment updates a attachment.
|
// UpdateAttachment updates a attachment.
|
||||||
rpc UpdateAttachment(UpdateAttachmentRequest) returns (Attachment) {
|
rpc UpdateAttachment(UpdateAttachmentRequest) returns (Attachment) {
|
||||||
option (google.api.http) = {
|
option (google.api.http) = {
|
||||||
|
|
@ -138,21 +132,6 @@ message GetAttachmentRequest {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
message UpdateAttachmentRequest {
|
||||||
// Required. The attachment which replaces the attachment on the server.
|
// Required. The attachment which replaces the attachment on the server.
|
||||||
Attachment attachment = 1 [(google.api.field_behavior) = REQUIRED];
|
Attachment attachment = 1 [(google.api.field_behavior) = REQUIRED];
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import "api/v1/common.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";
|
||||||
import "google/api/httpbody.proto";
|
|
||||||
import "google/api/resource.proto";
|
import "google/api/resource.proto";
|
||||||
import "google/protobuf/empty.proto";
|
import "google/protobuf/empty.proto";
|
||||||
import "google/protobuf/field_mask.proto";
|
import "google/protobuf/field_mask.proto";
|
||||||
|
|
@ -53,12 +52,6 @@ service UserService {
|
||||||
option (google.api.method_signature) = "name";
|
option (google.api.method_signature) = "name";
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserAvatar gets the avatar of a user.
|
|
||||||
rpc GetUserAvatar(GetUserAvatarRequest) returns (google.api.HttpBody) {
|
|
||||||
option (google.api.http) = {get: "/api/v1/{name=users/*}/avatar"};
|
|
||||||
option (google.api.method_signature) = "name";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListAllUserStats returns statistics for all users.
|
// ListAllUserStats returns statistics for all users.
|
||||||
rpc ListAllUserStats(ListAllUserStatsRequest) returns (ListAllUserStatsResponse) {
|
rpc ListAllUserStats(ListAllUserStatsRequest) returns (ListAllUserStatsResponse) {
|
||||||
option (google.api.http) = {get: "/api/v1/users:stats"};
|
option (google.api.http) = {get: "/api/v1/users:stats"};
|
||||||
|
|
@ -324,15 +317,6 @@ message DeleteUserRequest {
|
||||||
bool force = 2 [(google.api.field_behavior) = OPTIONAL];
|
bool force = 2 [(google.api.field_behavior) = OPTIONAL];
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetUserAvatarRequest {
|
|
||||||
// Required. The resource name of the user.
|
|
||||||
// Format: users/{user}
|
|
||||||
string name = 1 [
|
|
||||||
(google.api.field_behavior) = REQUIRED,
|
|
||||||
(google.api.resource_reference) = {type: "memos.api.v1/User"}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// User statistics messages
|
// User statistics messages
|
||||||
message UserStats {
|
message UserStats {
|
||||||
option (google.api.resource) = {
|
option (google.api.resource) = {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ package apiv1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "google.golang.org/genproto/googleapis/api/annotations"
|
_ "google.golang.org/genproto/googleapis/api/annotations"
|
||||||
httpbody "google.golang.org/genproto/googleapis/api/httpbody"
|
|
||||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
|
@ -381,70 +380,6 @@ func (x *GetAttachmentRequest) GetName() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetAttachmentBinaryRequest struct {
|
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
|
||||||
// Required. The attachment name of the attachment.
|
|
||||||
// Format: attachments/{attachment}
|
|
||||||
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
|
|
||||||
// The filename of the attachment. Mainly used for downloading.
|
|
||||||
Filename string `protobuf:"bytes,2,opt,name=filename,proto3" json:"filename,omitempty"`
|
|
||||||
// Optional. A flag indicating if the thumbnail version of the attachment should be returned.
|
|
||||||
Thumbnail bool `protobuf:"varint,3,opt,name=thumbnail,proto3" json:"thumbnail,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
|
||||||
sizeCache protoimpl.SizeCache
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GetAttachmentBinaryRequest) Reset() {
|
|
||||||
*x = GetAttachmentBinaryRequest{}
|
|
||||||
mi := &file_api_v1_attachment_service_proto_msgTypes[5]
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GetAttachmentBinaryRequest) String() string {
|
|
||||||
return protoimpl.X.MessageStringOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*GetAttachmentBinaryRequest) ProtoMessage() {}
|
|
||||||
|
|
||||||
func (x *GetAttachmentBinaryRequest) ProtoReflect() protoreflect.Message {
|
|
||||||
mi := &file_api_v1_attachment_service_proto_msgTypes[5]
|
|
||||||
if x != nil {
|
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
|
||||||
if ms.LoadMessageInfo() == nil {
|
|
||||||
ms.StoreMessageInfo(mi)
|
|
||||||
}
|
|
||||||
return ms
|
|
||||||
}
|
|
||||||
return mi.MessageOf(x)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deprecated: Use GetAttachmentBinaryRequest.ProtoReflect.Descriptor instead.
|
|
||||||
func (*GetAttachmentBinaryRequest) Descriptor() ([]byte, []int) {
|
|
||||||
return file_api_v1_attachment_service_proto_rawDescGZIP(), []int{5}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GetAttachmentBinaryRequest) GetName() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.Name
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GetAttachmentBinaryRequest) GetFilename() string {
|
|
||||||
if x != nil {
|
|
||||||
return x.Filename
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *GetAttachmentBinaryRequest) GetThumbnail() bool {
|
|
||||||
if x != nil {
|
|
||||||
return x.Thumbnail
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type UpdateAttachmentRequest struct {
|
type UpdateAttachmentRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
// Required. The attachment which replaces the attachment on the server.
|
// Required. The attachment which replaces the attachment on the server.
|
||||||
|
|
@ -457,7 +392,7 @@ type UpdateAttachmentRequest struct {
|
||||||
|
|
||||||
func (x *UpdateAttachmentRequest) Reset() {
|
func (x *UpdateAttachmentRequest) Reset() {
|
||||||
*x = UpdateAttachmentRequest{}
|
*x = UpdateAttachmentRequest{}
|
||||||
mi := &file_api_v1_attachment_service_proto_msgTypes[6]
|
mi := &file_api_v1_attachment_service_proto_msgTypes[5]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -469,7 +404,7 @@ func (x *UpdateAttachmentRequest) String() string {
|
||||||
func (*UpdateAttachmentRequest) ProtoMessage() {}
|
func (*UpdateAttachmentRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *UpdateAttachmentRequest) ProtoReflect() protoreflect.Message {
|
func (x *UpdateAttachmentRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_api_v1_attachment_service_proto_msgTypes[6]
|
mi := &file_api_v1_attachment_service_proto_msgTypes[5]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -482,7 +417,7 @@ func (x *UpdateAttachmentRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use UpdateAttachmentRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use UpdateAttachmentRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*UpdateAttachmentRequest) Descriptor() ([]byte, []int) {
|
func (*UpdateAttachmentRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_api_v1_attachment_service_proto_rawDescGZIP(), []int{6}
|
return file_api_v1_attachment_service_proto_rawDescGZIP(), []int{5}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *UpdateAttachmentRequest) GetAttachment() *Attachment {
|
func (x *UpdateAttachmentRequest) GetAttachment() *Attachment {
|
||||||
|
|
@ -510,7 +445,7 @@ type DeleteAttachmentRequest struct {
|
||||||
|
|
||||||
func (x *DeleteAttachmentRequest) Reset() {
|
func (x *DeleteAttachmentRequest) Reset() {
|
||||||
*x = DeleteAttachmentRequest{}
|
*x = DeleteAttachmentRequest{}
|
||||||
mi := &file_api_v1_attachment_service_proto_msgTypes[7]
|
mi := &file_api_v1_attachment_service_proto_msgTypes[6]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -522,7 +457,7 @@ func (x *DeleteAttachmentRequest) String() string {
|
||||||
func (*DeleteAttachmentRequest) ProtoMessage() {}
|
func (*DeleteAttachmentRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *DeleteAttachmentRequest) ProtoReflect() protoreflect.Message {
|
func (x *DeleteAttachmentRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_api_v1_attachment_service_proto_msgTypes[7]
|
mi := &file_api_v1_attachment_service_proto_msgTypes[6]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -535,7 +470,7 @@ func (x *DeleteAttachmentRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use DeleteAttachmentRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use DeleteAttachmentRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*DeleteAttachmentRequest) Descriptor() ([]byte, []int) {
|
func (*DeleteAttachmentRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_api_v1_attachment_service_proto_rawDescGZIP(), []int{7}
|
return file_api_v1_attachment_service_proto_rawDescGZIP(), []int{6}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *DeleteAttachmentRequest) GetName() string {
|
func (x *DeleteAttachmentRequest) GetName() string {
|
||||||
|
|
@ -549,7 +484,7 @@ var File_api_v1_attachment_service_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
const file_api_v1_attachment_service_proto_rawDesc = "" +
|
const file_api_v1_attachment_service_proto_rawDesc = "" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"\x1fapi/v1/attachment_service.proto\x12\fmemos.api.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/httpbody.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xfb\x02\n" +
|
"\x1fapi/v1/attachment_service.proto\x12\fmemos.api.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xfb\x02\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"Attachment\x12\x17\n" +
|
"Attachment\x12\x17\n" +
|
||||||
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12@\n" +
|
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12@\n" +
|
||||||
|
|
@ -582,12 +517,7 @@ const file_api_v1_attachment_service_proto_rawDesc = "" +
|
||||||
"total_size\x18\x03 \x01(\x05R\ttotalSize\"K\n" +
|
"total_size\x18\x03 \x01(\x05R\ttotalSize\"K\n" +
|
||||||
"\x14GetAttachmentRequest\x123\n" +
|
"\x14GetAttachmentRequest\x123\n" +
|
||||||
"\x04name\x18\x01 \x01(\tB\x1f\xe0A\x02\xfaA\x19\n" +
|
"\x04name\x18\x01 \x01(\tB\x1f\xe0A\x02\xfaA\x19\n" +
|
||||||
"\x17memos.api.v1/AttachmentR\x04name\"\x95\x01\n" +
|
"\x17memos.api.v1/AttachmentR\x04name\"\x9a\x01\n" +
|
||||||
"\x1aGetAttachmentBinaryRequest\x123\n" +
|
|
||||||
"\x04name\x18\x01 \x01(\tB\x1f\xe0A\x02\xfaA\x19\n" +
|
|
||||||
"\x17memos.api.v1/AttachmentR\x04name\x12\x1f\n" +
|
|
||||||
"\bfilename\x18\x02 \x01(\tB\x03\xe0A\x02R\bfilename\x12!\n" +
|
|
||||||
"\tthumbnail\x18\x03 \x01(\bB\x03\xe0A\x01R\tthumbnail\"\x9a\x01\n" +
|
|
||||||
"\x17UpdateAttachmentRequest\x12=\n" +
|
"\x17UpdateAttachmentRequest\x12=\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"attachment\x18\x01 \x01(\v2\x18.memos.api.v1.AttachmentB\x03\xe0A\x02R\n" +
|
"attachment\x18\x01 \x01(\v2\x18.memos.api.v1.AttachmentB\x03\xe0A\x02R\n" +
|
||||||
|
|
@ -596,14 +526,13 @@ const file_api_v1_attachment_service_proto_rawDesc = "" +
|
||||||
"updateMask\"N\n" +
|
"updateMask\"N\n" +
|
||||||
"\x17DeleteAttachmentRequest\x123\n" +
|
"\x17DeleteAttachmentRequest\x123\n" +
|
||||||
"\x04name\x18\x01 \x01(\tB\x1f\xe0A\x02\xfaA\x19\n" +
|
"\x04name\x18\x01 \x01(\tB\x1f\xe0A\x02\xfaA\x19\n" +
|
||||||
"\x17memos.api.v1/AttachmentR\x04name2\xe5\x06\n" +
|
"\x17memos.api.v1/AttachmentR\x04name2\xc4\x05\n" +
|
||||||
"\x11AttachmentService\x12\x89\x01\n" +
|
"\x11AttachmentService\x12\x89\x01\n" +
|
||||||
"\x10CreateAttachment\x12%.memos.api.v1.CreateAttachmentRequest\x1a\x18.memos.api.v1.Attachment\"4\xdaA\n" +
|
"\x10CreateAttachment\x12%.memos.api.v1.CreateAttachmentRequest\x1a\x18.memos.api.v1.Attachment\"4\xdaA\n" +
|
||||||
"attachment\x82\xd3\xe4\x93\x02!:\n" +
|
"attachment\x82\xd3\xe4\x93\x02!:\n" +
|
||||||
"attachment\"\x13/api/v1/attachments\x12{\n" +
|
"attachment\"\x13/api/v1/attachments\x12{\n" +
|
||||||
"\x0fListAttachments\x12$.memos.api.v1.ListAttachmentsRequest\x1a%.memos.api.v1.ListAttachmentsResponse\"\x1b\x82\xd3\xe4\x93\x02\x15\x12\x13/api/v1/attachments\x12z\n" +
|
"\x0fListAttachments\x12$.memos.api.v1.ListAttachmentsRequest\x1a%.memos.api.v1.ListAttachmentsResponse\"\x1b\x82\xd3\xe4\x93\x02\x15\x12\x13/api/v1/attachments\x12z\n" +
|
||||||
"\rGetAttachment\x12\".memos.api.v1.GetAttachmentRequest\x1a\x18.memos.api.v1.Attachment\"+\xdaA\x04name\x82\xd3\xe4\x93\x02\x1e\x12\x1c/api/v1/{name=attachments/*}\x12\x9e\x01\n" +
|
"\rGetAttachment\x12\".memos.api.v1.GetAttachmentRequest\x1a\x18.memos.api.v1.Attachment\"+\xdaA\x04name\x82\xd3\xe4\x93\x02\x1e\x12\x1c/api/v1/{name=attachments/*}\x12\xa9\x01\n" +
|
||||||
"\x13GetAttachmentBinary\x12(.memos.api.v1.GetAttachmentBinaryRequest\x1a\x14.google.api.HttpBody\"G\xdaA\x17name,filename,thumbnail\x82\xd3\xe4\x93\x02'\x12%/file/{name=attachments/*}/{filename}\x12\xa9\x01\n" +
|
|
||||||
"\x10UpdateAttachment\x12%.memos.api.v1.UpdateAttachmentRequest\x1a\x18.memos.api.v1.Attachment\"T\xdaA\x16attachment,update_mask\x82\xd3\xe4\x93\x025:\n" +
|
"\x10UpdateAttachment\x12%.memos.api.v1.UpdateAttachmentRequest\x1a\x18.memos.api.v1.Attachment\"T\xdaA\x16attachment,update_mask\x82\xd3\xe4\x93\x025:\n" +
|
||||||
"attachment2'/api/v1/{attachment.name=attachments/*}\x12~\n" +
|
"attachment2'/api/v1/{attachment.name=attachments/*}\x12~\n" +
|
||||||
"\x10DeleteAttachment\x12%.memos.api.v1.DeleteAttachmentRequest\x1a\x16.google.protobuf.Empty\"+\xdaA\x04name\x82\xd3\xe4\x93\x02\x1e*\x1c/api/v1/{name=attachments/*}B\xae\x01\n" +
|
"\x10DeleteAttachment\x12%.memos.api.v1.DeleteAttachmentRequest\x1a\x16.google.protobuf.Empty\"+\xdaA\x04name\x82\xd3\xe4\x93\x02\x1e*\x1c/api/v1/{name=attachments/*}B\xae\x01\n" +
|
||||||
|
|
@ -621,41 +550,37 @@ func file_api_v1_attachment_service_proto_rawDescGZIP() []byte {
|
||||||
return file_api_v1_attachment_service_proto_rawDescData
|
return file_api_v1_attachment_service_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_api_v1_attachment_service_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
|
var file_api_v1_attachment_service_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
|
||||||
var file_api_v1_attachment_service_proto_goTypes = []any{
|
var file_api_v1_attachment_service_proto_goTypes = []any{
|
||||||
(*Attachment)(nil), // 0: memos.api.v1.Attachment
|
(*Attachment)(nil), // 0: memos.api.v1.Attachment
|
||||||
(*CreateAttachmentRequest)(nil), // 1: memos.api.v1.CreateAttachmentRequest
|
(*CreateAttachmentRequest)(nil), // 1: memos.api.v1.CreateAttachmentRequest
|
||||||
(*ListAttachmentsRequest)(nil), // 2: memos.api.v1.ListAttachmentsRequest
|
(*ListAttachmentsRequest)(nil), // 2: memos.api.v1.ListAttachmentsRequest
|
||||||
(*ListAttachmentsResponse)(nil), // 3: memos.api.v1.ListAttachmentsResponse
|
(*ListAttachmentsResponse)(nil), // 3: memos.api.v1.ListAttachmentsResponse
|
||||||
(*GetAttachmentRequest)(nil), // 4: memos.api.v1.GetAttachmentRequest
|
(*GetAttachmentRequest)(nil), // 4: memos.api.v1.GetAttachmentRequest
|
||||||
(*GetAttachmentBinaryRequest)(nil), // 5: memos.api.v1.GetAttachmentBinaryRequest
|
(*UpdateAttachmentRequest)(nil), // 5: memos.api.v1.UpdateAttachmentRequest
|
||||||
(*UpdateAttachmentRequest)(nil), // 6: memos.api.v1.UpdateAttachmentRequest
|
(*DeleteAttachmentRequest)(nil), // 6: memos.api.v1.DeleteAttachmentRequest
|
||||||
(*DeleteAttachmentRequest)(nil), // 7: memos.api.v1.DeleteAttachmentRequest
|
(*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp
|
||||||
(*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp
|
(*fieldmaskpb.FieldMask)(nil), // 8: google.protobuf.FieldMask
|
||||||
(*fieldmaskpb.FieldMask)(nil), // 9: google.protobuf.FieldMask
|
(*emptypb.Empty)(nil), // 9: google.protobuf.Empty
|
||||||
(*httpbody.HttpBody)(nil), // 10: google.api.HttpBody
|
|
||||||
(*emptypb.Empty)(nil), // 11: google.protobuf.Empty
|
|
||||||
}
|
}
|
||||||
var file_api_v1_attachment_service_proto_depIdxs = []int32{
|
var file_api_v1_attachment_service_proto_depIdxs = []int32{
|
||||||
8, // 0: memos.api.v1.Attachment.create_time:type_name -> google.protobuf.Timestamp
|
7, // 0: memos.api.v1.Attachment.create_time:type_name -> google.protobuf.Timestamp
|
||||||
0, // 1: memos.api.v1.CreateAttachmentRequest.attachment:type_name -> memos.api.v1.Attachment
|
0, // 1: memos.api.v1.CreateAttachmentRequest.attachment:type_name -> memos.api.v1.Attachment
|
||||||
0, // 2: memos.api.v1.ListAttachmentsResponse.attachments:type_name -> memos.api.v1.Attachment
|
0, // 2: memos.api.v1.ListAttachmentsResponse.attachments:type_name -> memos.api.v1.Attachment
|
||||||
0, // 3: memos.api.v1.UpdateAttachmentRequest.attachment:type_name -> memos.api.v1.Attachment
|
0, // 3: memos.api.v1.UpdateAttachmentRequest.attachment:type_name -> memos.api.v1.Attachment
|
||||||
9, // 4: memos.api.v1.UpdateAttachmentRequest.update_mask:type_name -> google.protobuf.FieldMask
|
8, // 4: memos.api.v1.UpdateAttachmentRequest.update_mask:type_name -> google.protobuf.FieldMask
|
||||||
1, // 5: memos.api.v1.AttachmentService.CreateAttachment:input_type -> memos.api.v1.CreateAttachmentRequest
|
1, // 5: memos.api.v1.AttachmentService.CreateAttachment:input_type -> memos.api.v1.CreateAttachmentRequest
|
||||||
2, // 6: memos.api.v1.AttachmentService.ListAttachments:input_type -> memos.api.v1.ListAttachmentsRequest
|
2, // 6: memos.api.v1.AttachmentService.ListAttachments:input_type -> memos.api.v1.ListAttachmentsRequest
|
||||||
4, // 7: memos.api.v1.AttachmentService.GetAttachment:input_type -> memos.api.v1.GetAttachmentRequest
|
4, // 7: memos.api.v1.AttachmentService.GetAttachment:input_type -> memos.api.v1.GetAttachmentRequest
|
||||||
5, // 8: memos.api.v1.AttachmentService.GetAttachmentBinary:input_type -> memos.api.v1.GetAttachmentBinaryRequest
|
5, // 8: memos.api.v1.AttachmentService.UpdateAttachment:input_type -> memos.api.v1.UpdateAttachmentRequest
|
||||||
6, // 9: memos.api.v1.AttachmentService.UpdateAttachment:input_type -> memos.api.v1.UpdateAttachmentRequest
|
6, // 9: memos.api.v1.AttachmentService.DeleteAttachment:input_type -> memos.api.v1.DeleteAttachmentRequest
|
||||||
7, // 10: memos.api.v1.AttachmentService.DeleteAttachment:input_type -> memos.api.v1.DeleteAttachmentRequest
|
0, // 10: memos.api.v1.AttachmentService.CreateAttachment:output_type -> memos.api.v1.Attachment
|
||||||
0, // 11: memos.api.v1.AttachmentService.CreateAttachment:output_type -> memos.api.v1.Attachment
|
3, // 11: memos.api.v1.AttachmentService.ListAttachments:output_type -> memos.api.v1.ListAttachmentsResponse
|
||||||
3, // 12: memos.api.v1.AttachmentService.ListAttachments:output_type -> memos.api.v1.ListAttachmentsResponse
|
0, // 12: memos.api.v1.AttachmentService.GetAttachment:output_type -> memos.api.v1.Attachment
|
||||||
0, // 13: memos.api.v1.AttachmentService.GetAttachment:output_type -> memos.api.v1.Attachment
|
0, // 13: memos.api.v1.AttachmentService.UpdateAttachment:output_type -> memos.api.v1.Attachment
|
||||||
10, // 14: memos.api.v1.AttachmentService.GetAttachmentBinary:output_type -> google.api.HttpBody
|
9, // 14: memos.api.v1.AttachmentService.DeleteAttachment:output_type -> google.protobuf.Empty
|
||||||
0, // 15: memos.api.v1.AttachmentService.UpdateAttachment:output_type -> memos.api.v1.Attachment
|
10, // [10:15] is the sub-list for method output_type
|
||||||
11, // 16: memos.api.v1.AttachmentService.DeleteAttachment:output_type -> google.protobuf.Empty
|
5, // [5:10] is the sub-list for method input_type
|
||||||
11, // [11:17] is the sub-list for method output_type
|
|
||||||
5, // [5:11] is the sub-list for method input_type
|
|
||||||
5, // [5:5] is the sub-list for extension type_name
|
5, // [5:5] is the sub-list for extension type_name
|
||||||
5, // [5:5] is the sub-list for extension extendee
|
5, // [5:5] is the sub-list for extension extendee
|
||||||
0, // [0:5] is the sub-list for field type_name
|
0, // [0:5] is the sub-list for field type_name
|
||||||
|
|
@ -673,7 +598,7 @@ func file_api_v1_attachment_service_proto_init() {
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_attachment_service_proto_rawDesc), len(file_api_v1_attachment_service_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_attachment_service_proto_rawDesc), len(file_api_v1_attachment_service_proto_rawDesc)),
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 8,
|
NumMessages: 7,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -150,75 +150,6 @@ func local_request_AttachmentService_GetAttachment_0(ctx context.Context, marsha
|
||||||
return msg, metadata, err
|
return msg, metadata, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var filter_AttachmentService_GetAttachmentBinary_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0, "filename": 1}, Base: []int{1, 1, 2, 0, 0}, Check: []int{0, 1, 1, 2, 3}}
|
|
||||||
|
|
||||||
func request_AttachmentService_GetAttachmentBinary_0(ctx context.Context, marshaler runtime.Marshaler, client AttachmentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
|
||||||
var (
|
|
||||||
protoReq GetAttachmentBinaryRequest
|
|
||||||
metadata runtime.ServerMetadata
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if req.Body != nil {
|
|
||||||
_, _ = io.Copy(io.Discard, req.Body)
|
|
||||||
}
|
|
||||||
val, ok := pathParams["name"]
|
|
||||||
if !ok {
|
|
||||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
|
|
||||||
}
|
|
||||||
protoReq.Name, err = runtime.String(val)
|
|
||||||
if err != nil {
|
|
||||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
|
|
||||||
}
|
|
||||||
val, ok = pathParams["filename"]
|
|
||||||
if !ok {
|
|
||||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "filename")
|
|
||||||
}
|
|
||||||
protoReq.Filename, err = runtime.String(val)
|
|
||||||
if err != nil {
|
|
||||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "filename", err)
|
|
||||||
}
|
|
||||||
if err := req.ParseForm(); err != nil {
|
|
||||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
|
||||||
}
|
|
||||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AttachmentService_GetAttachmentBinary_0); err != nil {
|
|
||||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
|
||||||
}
|
|
||||||
msg, err := client.GetAttachmentBinary(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
|
||||||
return msg, metadata, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func local_request_AttachmentService_GetAttachmentBinary_0(ctx context.Context, marshaler runtime.Marshaler, server AttachmentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
|
||||||
var (
|
|
||||||
protoReq GetAttachmentBinaryRequest
|
|
||||||
metadata runtime.ServerMetadata
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
val, ok := pathParams["name"]
|
|
||||||
if !ok {
|
|
||||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
|
|
||||||
}
|
|
||||||
protoReq.Name, err = runtime.String(val)
|
|
||||||
if err != nil {
|
|
||||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
|
|
||||||
}
|
|
||||||
val, ok = pathParams["filename"]
|
|
||||||
if !ok {
|
|
||||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "filename")
|
|
||||||
}
|
|
||||||
protoReq.Filename, err = runtime.String(val)
|
|
||||||
if err != nil {
|
|
||||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "filename", err)
|
|
||||||
}
|
|
||||||
if err := req.ParseForm(); err != nil {
|
|
||||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
|
||||||
}
|
|
||||||
if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AttachmentService_GetAttachmentBinary_0); err != nil {
|
|
||||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
|
|
||||||
}
|
|
||||||
msg, err := server.GetAttachmentBinary(ctx, &protoReq)
|
|
||||||
return msg, metadata, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var filter_AttachmentService_UpdateAttachment_0 = &utilities.DoubleArray{Encoding: map[string]int{"attachment": 0, "name": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}}
|
var filter_AttachmentService_UpdateAttachment_0 = &utilities.DoubleArray{Encoding: map[string]int{"attachment": 0, "name": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}}
|
||||||
|
|
||||||
func request_AttachmentService_UpdateAttachment_0(ctx context.Context, marshaler runtime.Marshaler, client AttachmentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
func request_AttachmentService_UpdateAttachment_0(ctx context.Context, marshaler runtime.Marshaler, client AttachmentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||||
|
|
@ -405,26 +336,6 @@ func RegisterAttachmentServiceHandlerServer(ctx context.Context, mux *runtime.Se
|
||||||
}
|
}
|
||||||
forward_AttachmentService_GetAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
forward_AttachmentService_GetAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||||
})
|
})
|
||||||
mux.Handle(http.MethodGet, pattern_AttachmentService_GetAttachmentBinary_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
|
||||||
ctx, cancel := context.WithCancel(req.Context())
|
|
||||||
defer cancel()
|
|
||||||
var stream runtime.ServerTransportStream
|
|
||||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
|
||||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
|
||||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.AttachmentService/GetAttachmentBinary", runtime.WithHTTPPathPattern("/file/{name=attachments/*}/{filename}"))
|
|
||||||
if err != nil {
|
|
||||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, md, err := local_request_AttachmentService_GetAttachmentBinary_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
|
||||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
|
||||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
|
||||||
if err != nil {
|
|
||||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
forward_AttachmentService_GetAttachmentBinary_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
|
||||||
})
|
|
||||||
mux.Handle(http.MethodPatch, pattern_AttachmentService_UpdateAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
mux.Handle(http.MethodPatch, pattern_AttachmentService_UpdateAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||||
ctx, cancel := context.WithCancel(req.Context())
|
ctx, cancel := context.WithCancel(req.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
@ -556,23 +467,6 @@ func RegisterAttachmentServiceHandlerClient(ctx context.Context, mux *runtime.Se
|
||||||
}
|
}
|
||||||
forward_AttachmentService_GetAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
forward_AttachmentService_GetAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||||
})
|
})
|
||||||
mux.Handle(http.MethodGet, pattern_AttachmentService_GetAttachmentBinary_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
|
||||||
ctx, cancel := context.WithCancel(req.Context())
|
|
||||||
defer cancel()
|
|
||||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
|
||||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.AttachmentService/GetAttachmentBinary", runtime.WithHTTPPathPattern("/file/{name=attachments/*}/{filename}"))
|
|
||||||
if err != nil {
|
|
||||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, md, err := request_AttachmentService_GetAttachmentBinary_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
|
||||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
|
||||||
if err != nil {
|
|
||||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
forward_AttachmentService_GetAttachmentBinary_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
|
||||||
})
|
|
||||||
mux.Handle(http.MethodPatch, pattern_AttachmentService_UpdateAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
mux.Handle(http.MethodPatch, pattern_AttachmentService_UpdateAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||||
ctx, cancel := context.WithCancel(req.Context())
|
ctx, cancel := context.WithCancel(req.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
@ -611,19 +505,17 @@ func RegisterAttachmentServiceHandlerClient(ctx context.Context, mux *runtime.Se
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
pattern_AttachmentService_CreateAttachment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "attachments"}, ""))
|
pattern_AttachmentService_CreateAttachment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "attachments"}, ""))
|
||||||
pattern_AttachmentService_ListAttachments_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "attachments"}, ""))
|
pattern_AttachmentService_ListAttachments_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "attachments"}, ""))
|
||||||
pattern_AttachmentService_GetAttachment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "attachments", "name"}, ""))
|
pattern_AttachmentService_GetAttachment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "attachments", "name"}, ""))
|
||||||
pattern_AttachmentService_GetAttachmentBinary_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 2, 5, 2, 1, 0, 4, 1, 5, 3}, []string{"file", "attachments", "name", "filename"}, ""))
|
pattern_AttachmentService_UpdateAttachment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "attachments", "attachment.name"}, ""))
|
||||||
pattern_AttachmentService_UpdateAttachment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "attachments", "attachment.name"}, ""))
|
pattern_AttachmentService_DeleteAttachment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "attachments", "name"}, ""))
|
||||||
pattern_AttachmentService_DeleteAttachment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "attachments", "name"}, ""))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
forward_AttachmentService_CreateAttachment_0 = runtime.ForwardResponseMessage
|
forward_AttachmentService_CreateAttachment_0 = runtime.ForwardResponseMessage
|
||||||
forward_AttachmentService_ListAttachments_0 = runtime.ForwardResponseMessage
|
forward_AttachmentService_ListAttachments_0 = runtime.ForwardResponseMessage
|
||||||
forward_AttachmentService_GetAttachment_0 = runtime.ForwardResponseMessage
|
forward_AttachmentService_GetAttachment_0 = runtime.ForwardResponseMessage
|
||||||
forward_AttachmentService_GetAttachmentBinary_0 = runtime.ForwardResponseMessage
|
forward_AttachmentService_UpdateAttachment_0 = runtime.ForwardResponseMessage
|
||||||
forward_AttachmentService_UpdateAttachment_0 = runtime.ForwardResponseMessage
|
forward_AttachmentService_DeleteAttachment_0 = runtime.ForwardResponseMessage
|
||||||
forward_AttachmentService_DeleteAttachment_0 = runtime.ForwardResponseMessage
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ package apiv1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
context "context"
|
context "context"
|
||||||
httpbody "google.golang.org/genproto/googleapis/api/httpbody"
|
|
||||||
grpc "google.golang.org/grpc"
|
grpc "google.golang.org/grpc"
|
||||||
codes "google.golang.org/grpc/codes"
|
codes "google.golang.org/grpc/codes"
|
||||||
status "google.golang.org/grpc/status"
|
status "google.golang.org/grpc/status"
|
||||||
|
|
@ -21,12 +20,11 @@ import (
|
||||||
const _ = grpc.SupportPackageIsVersion9
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AttachmentService_CreateAttachment_FullMethodName = "/memos.api.v1.AttachmentService/CreateAttachment"
|
AttachmentService_CreateAttachment_FullMethodName = "/memos.api.v1.AttachmentService/CreateAttachment"
|
||||||
AttachmentService_ListAttachments_FullMethodName = "/memos.api.v1.AttachmentService/ListAttachments"
|
AttachmentService_ListAttachments_FullMethodName = "/memos.api.v1.AttachmentService/ListAttachments"
|
||||||
AttachmentService_GetAttachment_FullMethodName = "/memos.api.v1.AttachmentService/GetAttachment"
|
AttachmentService_GetAttachment_FullMethodName = "/memos.api.v1.AttachmentService/GetAttachment"
|
||||||
AttachmentService_GetAttachmentBinary_FullMethodName = "/memos.api.v1.AttachmentService/GetAttachmentBinary"
|
AttachmentService_UpdateAttachment_FullMethodName = "/memos.api.v1.AttachmentService/UpdateAttachment"
|
||||||
AttachmentService_UpdateAttachment_FullMethodName = "/memos.api.v1.AttachmentService/UpdateAttachment"
|
AttachmentService_DeleteAttachment_FullMethodName = "/memos.api.v1.AttachmentService/DeleteAttachment"
|
||||||
AttachmentService_DeleteAttachment_FullMethodName = "/memos.api.v1.AttachmentService/DeleteAttachment"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AttachmentServiceClient is the client API for AttachmentService service.
|
// AttachmentServiceClient is the client API for AttachmentService service.
|
||||||
|
|
@ -39,8 +37,6 @@ type AttachmentServiceClient interface {
|
||||||
ListAttachments(ctx context.Context, in *ListAttachmentsRequest, opts ...grpc.CallOption) (*ListAttachmentsResponse, error)
|
ListAttachments(ctx context.Context, in *ListAttachmentsRequest, opts ...grpc.CallOption) (*ListAttachmentsResponse, error)
|
||||||
// GetAttachment returns a attachment by name.
|
// GetAttachment returns a attachment by name.
|
||||||
GetAttachment(ctx context.Context, in *GetAttachmentRequest, opts ...grpc.CallOption) (*Attachment, error)
|
GetAttachment(ctx context.Context, in *GetAttachmentRequest, opts ...grpc.CallOption) (*Attachment, error)
|
||||||
// GetAttachmentBinary returns a attachment binary by name.
|
|
||||||
GetAttachmentBinary(ctx context.Context, in *GetAttachmentBinaryRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error)
|
|
||||||
// UpdateAttachment updates a attachment.
|
// UpdateAttachment updates a attachment.
|
||||||
UpdateAttachment(ctx context.Context, in *UpdateAttachmentRequest, opts ...grpc.CallOption) (*Attachment, error)
|
UpdateAttachment(ctx context.Context, in *UpdateAttachmentRequest, opts ...grpc.CallOption) (*Attachment, error)
|
||||||
// DeleteAttachment deletes a attachment by name.
|
// DeleteAttachment deletes a attachment by name.
|
||||||
|
|
@ -85,16 +81,6 @@ func (c *attachmentServiceClient) GetAttachment(ctx context.Context, in *GetAtta
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *attachmentServiceClient) GetAttachmentBinary(ctx context.Context, in *GetAttachmentBinaryRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(httpbody.HttpBody)
|
|
||||||
err := c.cc.Invoke(ctx, AttachmentService_GetAttachmentBinary_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *attachmentServiceClient) UpdateAttachment(ctx context.Context, in *UpdateAttachmentRequest, opts ...grpc.CallOption) (*Attachment, error) {
|
func (c *attachmentServiceClient) UpdateAttachment(ctx context.Context, in *UpdateAttachmentRequest, opts ...grpc.CallOption) (*Attachment, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(Attachment)
|
out := new(Attachment)
|
||||||
|
|
@ -125,8 +111,6 @@ type AttachmentServiceServer interface {
|
||||||
ListAttachments(context.Context, *ListAttachmentsRequest) (*ListAttachmentsResponse, error)
|
ListAttachments(context.Context, *ListAttachmentsRequest) (*ListAttachmentsResponse, error)
|
||||||
// GetAttachment returns a attachment by name.
|
// GetAttachment returns a attachment by name.
|
||||||
GetAttachment(context.Context, *GetAttachmentRequest) (*Attachment, error)
|
GetAttachment(context.Context, *GetAttachmentRequest) (*Attachment, error)
|
||||||
// GetAttachmentBinary returns a attachment binary by name.
|
|
||||||
GetAttachmentBinary(context.Context, *GetAttachmentBinaryRequest) (*httpbody.HttpBody, error)
|
|
||||||
// UpdateAttachment updates a attachment.
|
// UpdateAttachment updates a attachment.
|
||||||
UpdateAttachment(context.Context, *UpdateAttachmentRequest) (*Attachment, error)
|
UpdateAttachment(context.Context, *UpdateAttachmentRequest) (*Attachment, error)
|
||||||
// DeleteAttachment deletes a attachment by name.
|
// DeleteAttachment deletes a attachment by name.
|
||||||
|
|
@ -150,9 +134,6 @@ func (UnimplementedAttachmentServiceServer) ListAttachments(context.Context, *Li
|
||||||
func (UnimplementedAttachmentServiceServer) GetAttachment(context.Context, *GetAttachmentRequest) (*Attachment, error) {
|
func (UnimplementedAttachmentServiceServer) GetAttachment(context.Context, *GetAttachmentRequest) (*Attachment, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method GetAttachment not implemented")
|
return nil, status.Error(codes.Unimplemented, "method GetAttachment not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedAttachmentServiceServer) GetAttachmentBinary(context.Context, *GetAttachmentBinaryRequest) (*httpbody.HttpBody, error) {
|
|
||||||
return nil, status.Error(codes.Unimplemented, "method GetAttachmentBinary not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedAttachmentServiceServer) UpdateAttachment(context.Context, *UpdateAttachmentRequest) (*Attachment, error) {
|
func (UnimplementedAttachmentServiceServer) UpdateAttachment(context.Context, *UpdateAttachmentRequest) (*Attachment, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method UpdateAttachment not implemented")
|
return nil, status.Error(codes.Unimplemented, "method UpdateAttachment not implemented")
|
||||||
}
|
}
|
||||||
|
|
@ -234,24 +215,6 @@ func _AttachmentService_GetAttachment_Handler(srv interface{}, ctx context.Conte
|
||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func _AttachmentService_GetAttachmentBinary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(GetAttachmentBinaryRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(AttachmentServiceServer).GetAttachmentBinary(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: AttachmentService_GetAttachmentBinary_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(AttachmentServiceServer).GetAttachmentBinary(ctx, req.(*GetAttachmentBinaryRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _AttachmentService_UpdateAttachment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _AttachmentService_UpdateAttachment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(UpdateAttachmentRequest)
|
in := new(UpdateAttachmentRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
|
|
@ -307,10 +270,6 @@ var AttachmentService_ServiceDesc = grpc.ServiceDesc{
|
||||||
MethodName: "GetAttachment",
|
MethodName: "GetAttachment",
|
||||||
Handler: _AttachmentService_GetAttachment_Handler,
|
Handler: _AttachmentService_GetAttachment_Handler,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
MethodName: "GetAttachmentBinary",
|
|
||||||
Handler: _AttachmentService_GetAttachmentBinary_Handler,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
MethodName: "UpdateAttachment",
|
MethodName: "UpdateAttachment",
|
||||||
Handler: _AttachmentService_UpdateAttachment_Handler,
|
Handler: _AttachmentService_UpdateAttachment_Handler,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -298,45 +298,6 @@ func local_request_UserService_DeleteUser_0(ctx context.Context, marshaler runti
|
||||||
return msg, metadata, err
|
return msg, metadata, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func request_UserService_GetUserAvatar_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
|
||||||
var (
|
|
||||||
protoReq GetUserAvatarRequest
|
|
||||||
metadata runtime.ServerMetadata
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if req.Body != nil {
|
|
||||||
_, _ = io.Copy(io.Discard, req.Body)
|
|
||||||
}
|
|
||||||
val, ok := pathParams["name"]
|
|
||||||
if !ok {
|
|
||||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
|
|
||||||
}
|
|
||||||
protoReq.Name, err = runtime.String(val)
|
|
||||||
if err != nil {
|
|
||||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
|
|
||||||
}
|
|
||||||
msg, err := client.GetUserAvatar(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
|
||||||
return msg, metadata, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func local_request_UserService_GetUserAvatar_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
|
||||||
var (
|
|
||||||
protoReq GetUserAvatarRequest
|
|
||||||
metadata runtime.ServerMetadata
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
val, ok := pathParams["name"]
|
|
||||||
if !ok {
|
|
||||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name")
|
|
||||||
}
|
|
||||||
protoReq.Name, err = runtime.String(val)
|
|
||||||
if err != nil {
|
|
||||||
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err)
|
|
||||||
}
|
|
||||||
msg, err := server.GetUserAvatar(ctx, &protoReq)
|
|
||||||
return msg, metadata, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func request_UserService_ListAllUserStats_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
func request_UserService_ListAllUserStats_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||||
var (
|
var (
|
||||||
protoReq ListAllUserStatsRequest
|
protoReq ListAllUserStatsRequest
|
||||||
|
|
@ -1282,26 +1243,6 @@ func RegisterUserServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux
|
||||||
}
|
}
|
||||||
forward_UserService_DeleteUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
forward_UserService_DeleteUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||||
})
|
})
|
||||||
mux.Handle(http.MethodGet, pattern_UserService_GetUserAvatar_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
|
||||||
ctx, cancel := context.WithCancel(req.Context())
|
|
||||||
defer cancel()
|
|
||||||
var stream runtime.ServerTransportStream
|
|
||||||
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
|
|
||||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
|
||||||
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/GetUserAvatar", runtime.WithHTTPPathPattern("/api/v1/{name=users/*}/avatar"))
|
|
||||||
if err != nil {
|
|
||||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, md, err := local_request_UserService_GetUserAvatar_0(annotatedContext, inboundMarshaler, server, req, pathParams)
|
|
||||||
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
|
|
||||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
|
||||||
if err != nil {
|
|
||||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
forward_UserService_GetUserAvatar_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
|
||||||
})
|
|
||||||
mux.Handle(http.MethodGet, pattern_UserService_ListAllUserStats_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
mux.Handle(http.MethodGet, pattern_UserService_ListAllUserStats_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||||
ctx, cancel := context.WithCancel(req.Context())
|
ctx, cancel := context.WithCancel(req.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
@ -1767,23 +1708,6 @@ func RegisterUserServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux
|
||||||
}
|
}
|
||||||
forward_UserService_DeleteUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
forward_UserService_DeleteUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||||
})
|
})
|
||||||
mux.Handle(http.MethodGet, pattern_UserService_GetUserAvatar_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
|
||||||
ctx, cancel := context.WithCancel(req.Context())
|
|
||||||
defer cancel()
|
|
||||||
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
|
|
||||||
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/GetUserAvatar", runtime.WithHTTPPathPattern("/api/v1/{name=users/*}/avatar"))
|
|
||||||
if err != nil {
|
|
||||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp, md, err := request_UserService_GetUserAvatar_0(annotatedContext, inboundMarshaler, client, req, pathParams)
|
|
||||||
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
|
|
||||||
if err != nil {
|
|
||||||
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
forward_UserService_GetUserAvatar_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
|
||||||
})
|
|
||||||
mux.Handle(http.MethodGet, pattern_UserService_ListAllUserStats_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
mux.Handle(http.MethodGet, pattern_UserService_ListAllUserStats_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||||
ctx, cancel := context.WithCancel(req.Context())
|
ctx, cancel := context.WithCancel(req.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
@ -2082,7 +2006,6 @@ var (
|
||||||
pattern_UserService_CreateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, ""))
|
pattern_UserService_CreateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, ""))
|
||||||
pattern_UserService_UpdateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "user.name"}, ""))
|
pattern_UserService_UpdateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "user.name"}, ""))
|
||||||
pattern_UserService_DeleteUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "name"}, ""))
|
pattern_UserService_DeleteUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "name"}, ""))
|
||||||
pattern_UserService_GetUserAvatar_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "name", "avatar"}, ""))
|
|
||||||
pattern_UserService_ListAllUserStats_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, "stats"))
|
pattern_UserService_ListAllUserStats_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, "stats"))
|
||||||
pattern_UserService_GetUserStats_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "name"}, "getStats"))
|
pattern_UserService_GetUserStats_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "name"}, "getStats"))
|
||||||
pattern_UserService_GetUserSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "settings", "name"}, ""))
|
pattern_UserService_GetUserSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "settings", "name"}, ""))
|
||||||
|
|
@ -2108,7 +2031,6 @@ var (
|
||||||
forward_UserService_CreateUser_0 = runtime.ForwardResponseMessage
|
forward_UserService_CreateUser_0 = runtime.ForwardResponseMessage
|
||||||
forward_UserService_UpdateUser_0 = runtime.ForwardResponseMessage
|
forward_UserService_UpdateUser_0 = runtime.ForwardResponseMessage
|
||||||
forward_UserService_DeleteUser_0 = runtime.ForwardResponseMessage
|
forward_UserService_DeleteUser_0 = runtime.ForwardResponseMessage
|
||||||
forward_UserService_GetUserAvatar_0 = runtime.ForwardResponseMessage
|
|
||||||
forward_UserService_ListAllUserStats_0 = runtime.ForwardResponseMessage
|
forward_UserService_ListAllUserStats_0 = runtime.ForwardResponseMessage
|
||||||
forward_UserService_GetUserStats_0 = runtime.ForwardResponseMessage
|
forward_UserService_GetUserStats_0 = runtime.ForwardResponseMessage
|
||||||
forward_UserService_GetUserSetting_0 = runtime.ForwardResponseMessage
|
forward_UserService_GetUserSetting_0 = runtime.ForwardResponseMessage
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ package apiv1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
context "context"
|
context "context"
|
||||||
httpbody "google.golang.org/genproto/googleapis/api/httpbody"
|
|
||||||
grpc "google.golang.org/grpc"
|
grpc "google.golang.org/grpc"
|
||||||
codes "google.golang.org/grpc/codes"
|
codes "google.golang.org/grpc/codes"
|
||||||
status "google.golang.org/grpc/status"
|
status "google.golang.org/grpc/status"
|
||||||
|
|
@ -26,7 +25,6 @@ const (
|
||||||
UserService_CreateUser_FullMethodName = "/memos.api.v1.UserService/CreateUser"
|
UserService_CreateUser_FullMethodName = "/memos.api.v1.UserService/CreateUser"
|
||||||
UserService_UpdateUser_FullMethodName = "/memos.api.v1.UserService/UpdateUser"
|
UserService_UpdateUser_FullMethodName = "/memos.api.v1.UserService/UpdateUser"
|
||||||
UserService_DeleteUser_FullMethodName = "/memos.api.v1.UserService/DeleteUser"
|
UserService_DeleteUser_FullMethodName = "/memos.api.v1.UserService/DeleteUser"
|
||||||
UserService_GetUserAvatar_FullMethodName = "/memos.api.v1.UserService/GetUserAvatar"
|
|
||||||
UserService_ListAllUserStats_FullMethodName = "/memos.api.v1.UserService/ListAllUserStats"
|
UserService_ListAllUserStats_FullMethodName = "/memos.api.v1.UserService/ListAllUserStats"
|
||||||
UserService_GetUserStats_FullMethodName = "/memos.api.v1.UserService/GetUserStats"
|
UserService_GetUserStats_FullMethodName = "/memos.api.v1.UserService/GetUserStats"
|
||||||
UserService_GetUserSetting_FullMethodName = "/memos.api.v1.UserService/GetUserSetting"
|
UserService_GetUserSetting_FullMethodName = "/memos.api.v1.UserService/GetUserSetting"
|
||||||
|
|
@ -63,8 +61,6 @@ type UserServiceClient interface {
|
||||||
UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*User, error)
|
UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*User, error)
|
||||||
// DeleteUser deletes a user.
|
// DeleteUser deletes a user.
|
||||||
DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
// GetUserAvatar gets the avatar of a user.
|
|
||||||
GetUserAvatar(ctx context.Context, in *GetUserAvatarRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error)
|
|
||||||
// ListAllUserStats returns statistics for all users.
|
// ListAllUserStats returns statistics for all users.
|
||||||
ListAllUserStats(ctx context.Context, in *ListAllUserStatsRequest, opts ...grpc.CallOption) (*ListAllUserStatsResponse, error)
|
ListAllUserStats(ctx context.Context, in *ListAllUserStatsRequest, opts ...grpc.CallOption) (*ListAllUserStatsResponse, error)
|
||||||
// GetUserStats returns statistics for a specific user.
|
// GetUserStats returns statistics for a specific user.
|
||||||
|
|
@ -159,16 +155,6 @@ func (c *userServiceClient) DeleteUser(ctx context.Context, in *DeleteUserReques
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *userServiceClient) GetUserAvatar(ctx context.Context, in *GetUserAvatarRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(httpbody.HttpBody)
|
|
||||||
err := c.cc.Invoke(ctx, UserService_GetUserAvatar_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *userServiceClient) ListAllUserStats(ctx context.Context, in *ListAllUserStatsRequest, opts ...grpc.CallOption) (*ListAllUserStatsResponse, error) {
|
func (c *userServiceClient) ListAllUserStats(ctx context.Context, in *ListAllUserStatsRequest, opts ...grpc.CallOption) (*ListAllUserStatsResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(ListAllUserStatsResponse)
|
out := new(ListAllUserStatsResponse)
|
||||||
|
|
@ -356,8 +342,6 @@ type UserServiceServer interface {
|
||||||
UpdateUser(context.Context, *UpdateUserRequest) (*User, error)
|
UpdateUser(context.Context, *UpdateUserRequest) (*User, error)
|
||||||
// DeleteUser deletes a user.
|
// DeleteUser deletes a user.
|
||||||
DeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error)
|
DeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error)
|
||||||
// GetUserAvatar gets the avatar of a user.
|
|
||||||
GetUserAvatar(context.Context, *GetUserAvatarRequest) (*httpbody.HttpBody, error)
|
|
||||||
// ListAllUserStats returns statistics for all users.
|
// ListAllUserStats returns statistics for all users.
|
||||||
ListAllUserStats(context.Context, *ListAllUserStatsRequest) (*ListAllUserStatsResponse, error)
|
ListAllUserStats(context.Context, *ListAllUserStatsRequest) (*ListAllUserStatsResponse, error)
|
||||||
// GetUserStats returns statistics for a specific user.
|
// GetUserStats returns statistics for a specific user.
|
||||||
|
|
@ -417,9 +401,6 @@ func (UnimplementedUserServiceServer) UpdateUser(context.Context, *UpdateUserReq
|
||||||
func (UnimplementedUserServiceServer) DeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error) {
|
func (UnimplementedUserServiceServer) DeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method DeleteUser not implemented")
|
return nil, status.Error(codes.Unimplemented, "method DeleteUser not implemented")
|
||||||
}
|
}
|
||||||
func (UnimplementedUserServiceServer) GetUserAvatar(context.Context, *GetUserAvatarRequest) (*httpbody.HttpBody, error) {
|
|
||||||
return nil, status.Error(codes.Unimplemented, "method GetUserAvatar not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedUserServiceServer) ListAllUserStats(context.Context, *ListAllUserStatsRequest) (*ListAllUserStatsResponse, error) {
|
func (UnimplementedUserServiceServer) ListAllUserStats(context.Context, *ListAllUserStatsRequest) (*ListAllUserStatsResponse, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method ListAllUserStats not implemented")
|
return nil, status.Error(codes.Unimplemented, "method ListAllUserStats not implemented")
|
||||||
}
|
}
|
||||||
|
|
@ -582,24 +563,6 @@ func _UserService_DeleteUser_Handler(srv interface{}, ctx context.Context, dec f
|
||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func _UserService_GetUserAvatar_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(GetUserAvatarRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(UserServiceServer).GetUserAvatar(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: UserService_GetUserAvatar_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(UserServiceServer).GetUserAvatar(ctx, req.(*GetUserAvatarRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _UserService_ListAllUserStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _UserService_ListAllUserStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(ListAllUserStatsRequest)
|
in := new(ListAllUserStatsRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
|
|
@ -933,10 +896,6 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
|
||||||
MethodName: "DeleteUser",
|
MethodName: "DeleteUser",
|
||||||
Handler: _UserService_DeleteUser_Handler,
|
Handler: _UserService_DeleteUser_Handler,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
MethodName: "GetUserAvatar",
|
|
||||||
Handler: _UserService_GetUserAvatar_Handler,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
MethodName: "ListAllUserStats",
|
MethodName: "ListAllUserStats",
|
||||||
Handler: _UserService_ListAllUserStats_Handler,
|
Handler: _UserService_ListAllUserStats_Handler,
|
||||||
|
|
|
||||||
|
|
@ -1322,30 +1322,6 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/Status'
|
$ref: '#/components/schemas/Status'
|
||||||
/api/v1/users/{user}/avatar:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- UserService
|
|
||||||
description: GetUserAvatar gets the avatar of a user.
|
|
||||||
operationId: UserService_GetUserAvatar
|
|
||||||
parameters:
|
|
||||||
- name: user
|
|
||||||
in: path
|
|
||||||
description: The user id.
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
content:
|
|
||||||
'*/*': {}
|
|
||||||
default:
|
|
||||||
description: Default error response
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Status'
|
|
||||||
/api/v1/users/{user}/notifications:
|
/api/v1/users/{user}/notifications:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
|
@ -1968,41 +1944,6 @@ paths:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/Status'
|
$ref: '#/components/schemas/Status'
|
||||||
/file/attachments/{attachment}/{filename:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- AttachmentService
|
|
||||||
description: GetAttachmentBinary returns a attachment binary by name.
|
|
||||||
operationId: AttachmentService_GetAttachmentBinary
|
|
||||||
parameters:
|
|
||||||
- name: filename
|
|
||||||
in: path
|
|
||||||
description: The filename of the attachment. Mainly used for downloading.
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- name: attachment
|
|
||||||
in: path
|
|
||||||
description: The attachment id.
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
- name: thumbnail
|
|
||||||
in: query
|
|
||||||
description: Optional. A flag indicating if the thumbnail version of the attachment should be returned.
|
|
||||||
schema:
|
|
||||||
type: boolean
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
content:
|
|
||||||
'*/*': {}
|
|
||||||
default:
|
|
||||||
description: Default error response
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/Status'
|
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
Activity:
|
Activity:
|
||||||
|
|
|
||||||
|
|
@ -6,21 +6,15 @@ import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
|
||||||
"github.com/lithammer/shortuuid/v4"
|
"github.com/lithammer/shortuuid/v4"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"google.golang.org/genproto/googleapis/api/httpbody"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/metadata"
|
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
@ -197,129 +191,6 @@ func (s *APIV1Service) GetAttachment(ctx context.Context, request *v1pb.GetAttac
|
||||||
return convertAttachmentFromStore(attachment), nil
|
return convertAttachmentFromStore(attachment), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *APIV1Service) GetAttachmentBinary(ctx context.Context, request *v1pb.GetAttachmentBinaryRequest) (*httpbody.HttpBody, error) {
|
|
||||||
attachmentUID, err := ExtractAttachmentUIDFromName(request.Name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invalid attachment id: %v", err)
|
|
||||||
}
|
|
||||||
attachment, err := s.Store.GetAttachment(ctx, &store.FindAttachment{
|
|
||||||
GetBlob: true,
|
|
||||||
UID: &attachmentUID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.Internal, "failed to get attachment: %v", err)
|
|
||||||
}
|
|
||||||
if attachment == nil {
|
|
||||||
return nil, status.Errorf(codes.NotFound, "attachment not found")
|
|
||||||
}
|
|
||||||
// Check the related memo visibility.
|
|
||||||
if attachment.MemoID != nil {
|
|
||||||
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
|
|
||||||
ID: attachment.MemoID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.Internal, "failed to find memo by ID: %v", attachment.MemoID)
|
|
||||||
}
|
|
||||||
if memo != nil && memo.Visibility != store.Public {
|
|
||||||
user, err := s.GetCurrentUser(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
|
|
||||||
}
|
|
||||||
if user == nil {
|
|
||||||
return nil, status.Errorf(codes.Unauthenticated, "unauthorized access")
|
|
||||||
}
|
|
||||||
if memo.Visibility == store.Private && user.ID != attachment.CreatorID {
|
|
||||||
return nil, status.Errorf(codes.Unauthenticated, "unauthorized access")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.Thumbnail && util.HasPrefixes(attachment.Type, SupportedThumbnailMimeTypes...) {
|
|
||||||
// Skip server-side thumbnail generation for S3 storage to reduce memory usage.
|
|
||||||
// S3 images use external links (presigned URLs) directly, which avoids:
|
|
||||||
// 1. Downloading large images from S3 into server memory
|
|
||||||
// 2. Decoding and resizing images on the server
|
|
||||||
// 3. High memory consumption when many thumbnails are requested at once
|
|
||||||
// The client will use the external link and can implement client-side thumbnail logic if needed.
|
|
||||||
if attachment.StorageType == storepb.AttachmentStorageType_S3 {
|
|
||||||
slog.Debug("skipping server-side thumbnail for S3-stored image to reduce memory usage")
|
|
||||||
// Fall through to return the full image via external link
|
|
||||||
} else {
|
|
||||||
// Generate thumbnails for local and database storage
|
|
||||||
thumbnailBlob, err := s.getOrGenerateThumbnail(ctx, attachment)
|
|
||||||
if err != nil {
|
|
||||||
// thumbnail failures are logged as warnings and not cosidered critical failures as
|
|
||||||
// a attachment image can be used in its place.
|
|
||||||
slog.Warn("failed to get attachment thumbnail image", slog.Any("error", err))
|
|
||||||
} else {
|
|
||||||
return &httpbody.HttpBody{
|
|
||||||
ContentType: attachment.Type,
|
|
||||||
Data: thumbnailBlob,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blob, err := s.GetAttachmentBlob(attachment)
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.Internal, "failed to get attachment blob: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType := attachment.Type
|
|
||||||
if strings.HasPrefix(contentType, "text/") {
|
|
||||||
contentType += "; charset=utf-8"
|
|
||||||
}
|
|
||||||
// Prevent XSS attacks by serving potentially unsafe files with a content type that prevents script execution.
|
|
||||||
if strings.EqualFold(contentType, "image/svg+xml") ||
|
|
||||||
strings.EqualFold(contentType, "text/html") ||
|
|
||||||
strings.EqualFold(contentType, "application/xhtml+xml") {
|
|
||||||
contentType = "application/octet-stream"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract range header from gRPC metadata for iOS Safari video support
|
|
||||||
var rangeHeader string
|
|
||||||
if md, ok := metadata.FromIncomingContext(ctx); ok {
|
|
||||||
// Check for range header from gRPC-Gateway
|
|
||||||
if ranges := md.Get("grpcgateway-range"); len(ranges) > 0 {
|
|
||||||
rangeHeader = ranges[0]
|
|
||||||
} else if ranges := md.Get("range"); len(ranges) > 0 {
|
|
||||||
rangeHeader = ranges[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log for debugging iOS Safari issues
|
|
||||||
if userAgents := md.Get("user-agent"); len(userAgents) > 0 {
|
|
||||||
userAgent := userAgents[0]
|
|
||||||
if strings.Contains(strings.ToLower(userAgent), "safari") && rangeHeader != "" {
|
|
||||||
slog.Debug("Safari range request detected",
|
|
||||||
slog.String("range", rangeHeader),
|
|
||||||
slog.String("user-agent", userAgent),
|
|
||||||
slog.String("content-type", contentType))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle range requests for video/audio streaming (iOS Safari requirement)
|
|
||||||
if rangeHeader != "" && (strings.HasPrefix(contentType, "video/") || strings.HasPrefix(contentType, "audio/")) {
|
|
||||||
return s.handleRangeRequest(ctx, blob, rangeHeader, contentType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set headers for streaming support
|
|
||||||
if strings.HasPrefix(contentType, "video/") || strings.HasPrefix(contentType, "audio/") {
|
|
||||||
if err := setResponseHeaders(ctx, map[string]string{
|
|
||||||
"accept-ranges": "bytes",
|
|
||||||
"content-length": fmt.Sprintf("%d", len(blob)),
|
|
||||||
"cache-control": "public, max-age=3600", // 1 hour cache
|
|
||||||
}); err != nil {
|
|
||||||
slog.Warn("failed to set streaming headers", slog.Any("error", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &httpbody.HttpBody{
|
|
||||||
ContentType: contentType,
|
|
||||||
Data: blob,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *APIV1Service) UpdateAttachment(ctx context.Context, request *v1pb.UpdateAttachmentRequest) (*v1pb.Attachment, error) {
|
func (s *APIV1Service) UpdateAttachment(ctx context.Context, request *v1pb.UpdateAttachmentRequest) (*v1pb.Attachment, error) {
|
||||||
attachmentUID, err := ExtractAttachmentUIDFromName(request.Attachment.Name)
|
attachmentUID, err := ExtractAttachmentUIDFromName(request.Attachment.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -541,113 +412,6 @@ func (s *APIV1Service) GetAttachmentBlob(attachment *store.Attachment) ([]byte,
|
||||||
return attachment.Blob, nil
|
return attachment.Blob, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
// thumbnailMaxSize is the maximum size in pixels for the largest dimension of the thumbnail image.
|
|
||||||
thumbnailMaxSize = 600
|
|
||||||
)
|
|
||||||
|
|
||||||
// getOrGenerateThumbnail returns the thumbnail image of the attachment.
|
|
||||||
// Uses semaphore to limit concurrent thumbnail generation and prevent memory exhaustion.
|
|
||||||
func (s *APIV1Service) getOrGenerateThumbnail(ctx context.Context, attachment *store.Attachment) ([]byte, error) {
|
|
||||||
thumbnailCacheFolder := filepath.Join(s.Profile.Data, ThumbnailCacheFolder)
|
|
||||||
if err := os.MkdirAll(thumbnailCacheFolder, os.ModePerm); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to create thumbnail cache folder")
|
|
||||||
}
|
|
||||||
filePath := filepath.Join(thumbnailCacheFolder, fmt.Sprintf("%d%s", attachment.ID, filepath.Ext(attachment.Filename)))
|
|
||||||
|
|
||||||
// Check if thumbnail already exists
|
|
||||||
if _, err := os.Stat(filePath); err == nil {
|
|
||||||
// Thumbnail exists, read and return it
|
|
||||||
thumbnailFile, err := os.Open(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to open thumbnail file")
|
|
||||||
}
|
|
||||||
defer thumbnailFile.Close()
|
|
||||||
blob, err := io.ReadAll(thumbnailFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to read thumbnail file")
|
|
||||||
}
|
|
||||||
return blob, nil
|
|
||||||
} else if !os.IsNotExist(err) {
|
|
||||||
return nil, errors.Wrap(err, "failed to check thumbnail image stat")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Thumbnail doesn't exist, acquire semaphore to limit concurrent generation
|
|
||||||
if err := s.thumbnailSemaphore.Acquire(ctx, 1); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to acquire thumbnail generation semaphore")
|
|
||||||
}
|
|
||||||
defer s.thumbnailSemaphore.Release(1)
|
|
||||||
|
|
||||||
// Double-check if thumbnail was created while waiting for semaphore
|
|
||||||
if _, err := os.Stat(filePath); err == nil {
|
|
||||||
thumbnailFile, err := os.Open(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to open thumbnail file")
|
|
||||||
}
|
|
||||||
defer thumbnailFile.Close()
|
|
||||||
blob, err := io.ReadAll(thumbnailFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to read thumbnail file")
|
|
||||||
}
|
|
||||||
return blob, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate the thumbnail
|
|
||||||
blob, err := s.GetAttachmentBlob(attachment)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to get attachment blob")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode image - this is memory intensive
|
|
||||||
img, err := imaging.Decode(bytes.NewReader(blob), imaging.AutoOrientation(true))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to decode thumbnail image")
|
|
||||||
}
|
|
||||||
|
|
||||||
// The largest dimension is set to thumbnailMaxSize and the smaller dimension is scaled proportionally.
|
|
||||||
// Small images are not enlarged.
|
|
||||||
width := img.Bounds().Dx()
|
|
||||||
height := img.Bounds().Dy()
|
|
||||||
var thumbnailWidth, thumbnailHeight int
|
|
||||||
|
|
||||||
// Only resize if the image is larger than thumbnailMaxSize
|
|
||||||
if max(width, height) > thumbnailMaxSize {
|
|
||||||
if width >= height {
|
|
||||||
// Landscape or square - constrain width, maintain aspect ratio for height
|
|
||||||
thumbnailWidth = thumbnailMaxSize
|
|
||||||
thumbnailHeight = 0
|
|
||||||
} else {
|
|
||||||
// Portrait - constrain height, maintain aspect ratio for width
|
|
||||||
thumbnailWidth = 0
|
|
||||||
thumbnailHeight = thumbnailMaxSize
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Keep original dimensions for small images
|
|
||||||
thumbnailWidth = width
|
|
||||||
thumbnailHeight = height
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resize the image to the calculated dimensions.
|
|
||||||
thumbnailImage := imaging.Resize(img, thumbnailWidth, thumbnailHeight, imaging.Lanczos)
|
|
||||||
|
|
||||||
// Save thumbnail to disk
|
|
||||||
if err := imaging.Save(thumbnailImage, filePath); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to save thumbnail file")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the saved thumbnail and return it
|
|
||||||
thumbnailFile, err := os.Open(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to open thumbnail file")
|
|
||||||
}
|
|
||||||
defer thumbnailFile.Close()
|
|
||||||
thumbnailBlob, err := io.ReadAll(thumbnailFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to read thumbnail file")
|
|
||||||
}
|
|
||||||
return thumbnailBlob, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var fileKeyPattern = regexp.MustCompile(`\{[a-z]{1,9}\}`)
|
var fileKeyPattern = regexp.MustCompile(`\{[a-z]{1,9}\}`)
|
||||||
|
|
||||||
func replaceFilenameWithPathTemplate(path, filename string) string {
|
func replaceFilenameWithPathTemplate(path, filename string) string {
|
||||||
|
|
@ -679,85 +443,6 @@ func replaceFilenameWithPathTemplate(path, filename string) string {
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleRangeRequest handles HTTP range requests for video/audio streaming (iOS Safari requirement).
|
|
||||||
func (*APIV1Service) handleRangeRequest(ctx context.Context, data []byte, rangeHeader, contentType string) (*httpbody.HttpBody, error) {
|
|
||||||
// Parse "bytes=start-end"
|
|
||||||
if !strings.HasPrefix(rangeHeader, "bytes=") {
|
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invalid range header format")
|
|
||||||
}
|
|
||||||
|
|
||||||
rangeSpec := strings.TrimPrefix(rangeHeader, "bytes=")
|
|
||||||
parts := strings.Split(rangeSpec, "-")
|
|
||||||
if len(parts) != 2 {
|
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invalid range specification")
|
|
||||||
}
|
|
||||||
|
|
||||||
fileSize := int64(len(data))
|
|
||||||
start, end := int64(0), fileSize-1
|
|
||||||
|
|
||||||
// Parse start position
|
|
||||||
if parts[0] != "" {
|
|
||||||
if s, err := strconv.ParseInt(parts[0], 10, 64); err == nil {
|
|
||||||
start = s
|
|
||||||
} else {
|
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invalid range start: %s", parts[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse end position
|
|
||||||
if parts[1] != "" {
|
|
||||||
if e, err := strconv.ParseInt(parts[1], 10, 64); err == nil {
|
|
||||||
end = e
|
|
||||||
} else {
|
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invalid range end: %s", parts[1])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate range
|
|
||||||
if start < 0 || end >= fileSize || start > end {
|
|
||||||
// Set Content-Range header for 416 response
|
|
||||||
if err := setResponseHeaders(ctx, map[string]string{
|
|
||||||
"content-range": fmt.Sprintf("bytes */%d", fileSize),
|
|
||||||
}); err != nil {
|
|
||||||
slog.Warn("failed to set content-range header", slog.Any("error", err))
|
|
||||||
}
|
|
||||||
return nil, status.Errorf(codes.OutOfRange, "requested range not satisfiable")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set partial content headers (HTTP 206)
|
|
||||||
if err := setResponseHeaders(ctx, map[string]string{
|
|
||||||
"accept-ranges": "bytes",
|
|
||||||
"content-range": fmt.Sprintf("bytes %d-%d/%d", start, end, fileSize),
|
|
||||||
"content-length": fmt.Sprintf("%d", end-start+1),
|
|
||||||
"cache-control": "public, max-age=3600",
|
|
||||||
}); err != nil {
|
|
||||||
slog.Warn("failed to set partial content headers", slog.Any("error", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the requested range
|
|
||||||
rangeData := data[start : end+1]
|
|
||||||
|
|
||||||
slog.Debug("serving partial content",
|
|
||||||
slog.Int64("start", start),
|
|
||||||
slog.Int64("end", end),
|
|
||||||
slog.Int64("total", fileSize),
|
|
||||||
slog.Int("chunk_size", len(rangeData)))
|
|
||||||
|
|
||||||
return &httpbody.HttpBody{
|
|
||||||
ContentType: contentType,
|
|
||||||
Data: rangeData,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// setResponseHeaders is a helper function to set gRPC response headers.
|
|
||||||
func setResponseHeaders(ctx context.Context, headers map[string]string) error {
|
|
||||||
pairs := make([]string, 0, len(headers)*2)
|
|
||||||
for key, value := range headers {
|
|
||||||
pairs = append(pairs, key, value)
|
|
||||||
}
|
|
||||||
return grpc.SetHeader(ctx, metadata.Pairs(pairs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateFilename(filename string) bool {
|
func validateFilename(filename string) bool {
|
||||||
// Reject path traversal attempts and make sure no additional directories are created
|
// Reject path traversal attempts and make sure no additional directories are created
|
||||||
if !filepath.IsLocal(filename) || strings.ContainsAny(filename, "/\\") {
|
if !filepath.IsLocal(filename) || strings.ContainsAny(filename, "/\\") {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package v1
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -19,7 +18,6 @@ import (
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"google.golang.org/genproto/googleapis/api/httpbody"
|
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
|
|
@ -106,39 +104,6 @@ func (s *APIV1Service) GetUser(ctx context.Context, request *v1pb.GetUserRequest
|
||||||
return convertUserFromStore(user), nil
|
return convertUserFromStore(user), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *APIV1Service) GetUserAvatar(ctx context.Context, request *v1pb.GetUserAvatarRequest) (*httpbody.HttpBody, error) {
|
|
||||||
userID, err := ExtractUserIDFromName(request.Name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err)
|
|
||||||
}
|
|
||||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
|
||||||
ID: &userID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
|
|
||||||
}
|
|
||||||
if user == nil {
|
|
||||||
return nil, status.Errorf(codes.NotFound, "user not found")
|
|
||||||
}
|
|
||||||
if user.AvatarURL == "" {
|
|
||||||
return nil, status.Errorf(codes.NotFound, "avatar not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
imageType, base64Data, err := extractImageInfo(user.AvatarURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.Internal, "failed to extract image info: %v", err)
|
|
||||||
}
|
|
||||||
imageData, err := base64.StdEncoding.DecodeString(base64Data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.Internal, "failed to decode string: %v", err)
|
|
||||||
}
|
|
||||||
httpBody := &httpbody.HttpBody{
|
|
||||||
ContentType: imageType,
|
|
||||||
Data: imageData,
|
|
||||||
}
|
|
||||||
return httpBody, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *APIV1Service) CreateUser(ctx context.Context, request *v1pb.CreateUserRequest) (*v1pb.User, error) {
|
func (s *APIV1Service) CreateUser(ctx context.Context, request *v1pb.CreateUserRequest) (*v1pb.User, error) {
|
||||||
// Get current user (might be nil for unauthenticated requests)
|
// Get current user (might be nil for unauthenticated requests)
|
||||||
currentUser, _ := s.GetCurrentUser(ctx)
|
currentUser, _ := s.GetCurrentUser(ctx)
|
||||||
|
|
@ -1151,7 +1116,7 @@ func convertUserFromStore(user *store.User) *v1pb.User {
|
||||||
// Check if avatar url is base64 format.
|
// Check if avatar url is base64 format.
|
||||||
_, _, err := extractImageInfo(user.AvatarURL)
|
_, _, err := extractImageInfo(user.AvatarURL)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
userpb.AvatarUrl = fmt.Sprintf("/api/v1/%s/avatar", userpb.Name)
|
userpb.AvatarUrl = fmt.Sprintf("/file/%s/avatar", userpb.Name)
|
||||||
} else {
|
} else {
|
||||||
userpb.AvatarUrl = user.AvatarURL
|
userpb.AvatarUrl = user.AvatarURL
|
||||||
}
|
}
|
||||||
|
|
@ -1183,11 +1148,13 @@ func convertUserRoleToStore(role v1pb.User_Role) store.Role {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractImageInfo extracts image type and base64 data from a data URI.
|
||||||
|
// Data URI format: data:image/png;base64,iVBORw0KGgo...
|
||||||
func extractImageInfo(dataURI string) (string, string, error) {
|
func extractImageInfo(dataURI string) (string, string, error) {
|
||||||
dataURIRegex := regexp.MustCompile(`^data:(?P<type>.+);base64,(?P<base64>.+)`)
|
dataURIRegex := regexp.MustCompile(`^data:(?P<type>.+);base64,(?P<base64>.+)`)
|
||||||
matches := dataURIRegex.FindStringSubmatch(dataURI)
|
matches := dataURIRegex.FindStringSubmatch(dataURI)
|
||||||
if len(matches) != 3 {
|
if len(matches) != 3 {
|
||||||
return "", "", errors.New("Invalid data URI format")
|
return "", "", errors.New("invalid data URI format")
|
||||||
}
|
}
|
||||||
imageType := matches[1]
|
imageType := matches[1]
|
||||||
base64Data := matches[2]
|
base64Data := matches[2]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,309 @@
|
||||||
|
# Fileserver Package
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The `fileserver` package handles all binary file serving for Memos using native HTTP handlers. It was created to replace gRPC-based binary serving, which had limitations with HTTP range requests (required for Safari video/audio playback).
|
||||||
|
|
||||||
|
## Responsibilities
|
||||||
|
|
||||||
|
- Serve attachment binary files (images, videos, audio, documents)
|
||||||
|
- Serve user avatar images
|
||||||
|
- Handle HTTP range requests for video/audio streaming
|
||||||
|
- Authenticate requests using session cookies or JWT tokens
|
||||||
|
- Check permissions for private content
|
||||||
|
- Generate and serve image thumbnails
|
||||||
|
- Prevent XSS attacks on uploaded content
|
||||||
|
- Support S3 external storage
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Design Principles
|
||||||
|
|
||||||
|
1. **Separation of Concerns**: Binary files via HTTP, metadata via gRPC
|
||||||
|
2. **DRY**: Imports auth constants from `api/v1` package (single source of truth)
|
||||||
|
3. **Security First**: Authentication, authorization, and XSS prevention
|
||||||
|
4. **Performance**: Native HTTP streaming with proper caching headers
|
||||||
|
|
||||||
|
### Package Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
fileserver/
|
||||||
|
├── fileserver.go # Main service and HTTP handlers
|
||||||
|
├── README.md # This file
|
||||||
|
└── fileserver_test.go # Tests (to be added)
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### 1. Attachment Binary
|
||||||
|
```
|
||||||
|
GET /file/attachments/:uid/:filename[?thumbnail=true]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `uid` - Attachment unique identifier
|
||||||
|
- `filename` - Original filename
|
||||||
|
- `thumbnail` (optional) - Return thumbnail for images
|
||||||
|
|
||||||
|
**Authentication:** Required for non-public memos
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
- `200 OK` - File content with proper Content-Type
|
||||||
|
- `206 Partial Content` - For range requests (video/audio)
|
||||||
|
- `401 Unauthorized` - Authentication required
|
||||||
|
- `403 Forbidden` - User not authorized
|
||||||
|
- `404 Not Found` - Attachment not found
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
- `Content-Type` - MIME type of the file
|
||||||
|
- `Cache-Control: public, max-age=3600`
|
||||||
|
- `Accept-Ranges: bytes` - For video/audio
|
||||||
|
- `Content-Range` - For partial responses (206)
|
||||||
|
|
||||||
|
### 2. User Avatar
|
||||||
|
```
|
||||||
|
GET /file/users/:identifier/avatar
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `identifier` - User ID (e.g., `1`) or username (e.g., `steven`)
|
||||||
|
|
||||||
|
**Authentication:** Not required (avatars are public)
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
- `200 OK` - Avatar image (PNG/JPEG)
|
||||||
|
- `404 Not Found` - User not found or no avatar set
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
- `Content-Type` - image/png or image/jpeg
|
||||||
|
- `Cache-Control: public, max-age=3600`
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### Supported Methods
|
||||||
|
|
||||||
|
The fileserver supports two authentication methods, checked in order:
|
||||||
|
|
||||||
|
1. **Session Cookie** (`user_session`)
|
||||||
|
- Cookie format: `{userID}-{sessionID}`
|
||||||
|
- Validates session exists and hasn't expired (14-day sliding window)
|
||||||
|
- Updates last accessed time on success
|
||||||
|
|
||||||
|
2. **JWT Bearer Token** (`Authorization: Bearer {token}`)
|
||||||
|
- Validates JWT signature using server secret
|
||||||
|
- Checks token exists in user's access tokens (for revocation)
|
||||||
|
- Extracts user ID from token claims
|
||||||
|
|
||||||
|
### Authentication Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Request → getCurrentUser()
|
||||||
|
├─→ Try Session Cookie
|
||||||
|
│ ├─→ Parse cookie value
|
||||||
|
│ ├─→ Get user from DB
|
||||||
|
│ ├─→ Validate session
|
||||||
|
│ └─→ Return user (if valid)
|
||||||
|
│
|
||||||
|
└─→ Try JWT Token
|
||||||
|
├─→ Parse Authorization header
|
||||||
|
├─→ Verify JWT signature
|
||||||
|
├─→ Get user from DB
|
||||||
|
├─→ Validate token in access tokens list
|
||||||
|
└─→ Return user (if valid)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Permission Model
|
||||||
|
|
||||||
|
**Attachments:**
|
||||||
|
- Unlinked: Public (no auth required)
|
||||||
|
- Public memo: Public (no auth required)
|
||||||
|
- Protected memo: Requires authentication
|
||||||
|
- Private memo: Creator only
|
||||||
|
|
||||||
|
**Avatars:**
|
||||||
|
- Always public (no auth required)
|
||||||
|
|
||||||
|
## Key Functions
|
||||||
|
|
||||||
|
### HTTP Handlers
|
||||||
|
|
||||||
|
#### `serveAttachmentFile(c echo.Context) error`
|
||||||
|
Main handler for attachment binary serving.
|
||||||
|
|
||||||
|
**Flow:**
|
||||||
|
1. Extract UID from URL parameter
|
||||||
|
2. Fetch attachment from database
|
||||||
|
3. Check permissions (memo visibility)
|
||||||
|
4. Get binary blob (local file, S3, or database)
|
||||||
|
5. Handle thumbnail request (if applicable)
|
||||||
|
6. Set security headers (XSS prevention)
|
||||||
|
7. Serve with range request support (video/audio)
|
||||||
|
|
||||||
|
#### `serveUserAvatar(c echo.Context) error`
|
||||||
|
Main handler for user avatar serving.
|
||||||
|
|
||||||
|
**Flow:**
|
||||||
|
1. Extract identifier (ID or username) from URL
|
||||||
|
2. Lookup user in database
|
||||||
|
3. Check if avatar exists
|
||||||
|
4. Decode base64 data URI
|
||||||
|
5. Serve with proper content type and caching
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
#### `getCurrentUser(ctx, c) (*store.User, error)`
|
||||||
|
Authenticates request using session cookie or JWT token.
|
||||||
|
|
||||||
|
#### `authenticateBySession(ctx, cookie) (*store.User, error)`
|
||||||
|
Validates session cookie and returns authenticated user.
|
||||||
|
|
||||||
|
#### `authenticateByJWT(ctx, token) (*store.User, error)`
|
||||||
|
Validates JWT access token and returns authenticated user.
|
||||||
|
|
||||||
|
### Permission Checks
|
||||||
|
|
||||||
|
#### `checkAttachmentPermission(ctx, c, attachment) error`
|
||||||
|
Validates user has permission to access attachment based on memo visibility.
|
||||||
|
|
||||||
|
### File Operations
|
||||||
|
|
||||||
|
#### `getAttachmentBlob(attachment) ([]byte, error)`
|
||||||
|
Retrieves binary content from local storage, S3, or database.
|
||||||
|
|
||||||
|
#### `getOrGenerateThumbnail(ctx, attachment) ([]byte, error)`
|
||||||
|
Returns cached thumbnail or generates new one (with semaphore limiting).
|
||||||
|
|
||||||
|
### Utilities
|
||||||
|
|
||||||
|
#### `getUserByIdentifier(ctx, identifier) (*store.User, error)`
|
||||||
|
Finds user by ID (int) or username (string).
|
||||||
|
|
||||||
|
#### `extractImageInfo(dataURI) (type, base64, error)`
|
||||||
|
Parses data URI to extract MIME type and base64 data.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### External Packages
|
||||||
|
- `github.com/labstack/echo/v4` - HTTP router and middleware
|
||||||
|
- `github.com/golang-jwt/jwt/v5` - JWT parsing and validation
|
||||||
|
- `github.com/disintegration/imaging` - Image thumbnail generation
|
||||||
|
- `golang.org/x/sync/semaphore` - Concurrency control for thumbnails
|
||||||
|
|
||||||
|
### Internal Packages
|
||||||
|
- `server/router/api/v1` - Auth constants (SessionCookieName, ClaimsMessage, etc.)
|
||||||
|
- `store` - Database operations
|
||||||
|
- `internal/profile` - Server configuration
|
||||||
|
- `plugin/storage/s3` - S3 storage client
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Constants
|
||||||
|
|
||||||
|
All auth-related constants are imported from `server/router/api/v1/auth.go`:
|
||||||
|
- `apiv1.SessionCookieName` - "user_session"
|
||||||
|
- `apiv1.SessionSlidingDuration` - 14 days
|
||||||
|
- `apiv1.KeyID` - "v1" (JWT key identifier)
|
||||||
|
- `apiv1.ClaimsMessage` - JWT claims struct
|
||||||
|
|
||||||
|
Package-specific constants:
|
||||||
|
- `ThumbnailCacheFolder` - ".thumbnail_cache"
|
||||||
|
- `thumbnailMaxSize` - 600px
|
||||||
|
- `SupportedThumbnailMimeTypes` - ["image/png", "image/jpeg"]
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
All handlers return Echo HTTP errors with appropriate status codes:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Bad request
|
||||||
|
echo.NewHTTPError(http.StatusBadRequest, "message")
|
||||||
|
|
||||||
|
// Unauthorized (no auth)
|
||||||
|
echo.NewHTTPError(http.StatusUnauthorized, "message")
|
||||||
|
|
||||||
|
// Forbidden (auth but no permission)
|
||||||
|
echo.NewHTTPError(http.StatusForbidden, "message")
|
||||||
|
|
||||||
|
// Not found
|
||||||
|
echo.NewHTTPError(http.StatusNotFound, "message")
|
||||||
|
|
||||||
|
// Internal error
|
||||||
|
echo.NewHTTPError(http.StatusInternalServerError, "message").SetInternal(err)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### 1. XSS Prevention
|
||||||
|
SVG and HTML files are served as `application/octet-stream` to prevent script execution:
|
||||||
|
|
||||||
|
```go
|
||||||
|
if contentType == "image/svg+xml" ||
|
||||||
|
contentType == "text/html" ||
|
||||||
|
contentType == "application/xhtml+xml" {
|
||||||
|
contentType = "application/octet-stream"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Authentication
|
||||||
|
Private content requires valid session or JWT token.
|
||||||
|
|
||||||
|
### 3. Authorization
|
||||||
|
Memo visibility rules enforced before serving attachments.
|
||||||
|
|
||||||
|
### 4. Input Validation
|
||||||
|
- Attachment UID validated from database
|
||||||
|
- User identifier validated (ID or username)
|
||||||
|
- Range requests validated before processing
|
||||||
|
|
||||||
|
## Performance Optimizations
|
||||||
|
|
||||||
|
### 1. Thumbnail Caching
|
||||||
|
Thumbnails cached on disk to avoid regeneration:
|
||||||
|
- Cache location: `{data_dir}/.thumbnail_cache/`
|
||||||
|
- Filename: `{attachment_id}{extension}`
|
||||||
|
- Semaphore limits concurrent generation (max 3)
|
||||||
|
|
||||||
|
### 2. HTTP Range Requests
|
||||||
|
Video/audio files use `http.ServeContent()` for efficient streaming:
|
||||||
|
- Automatic range parsing
|
||||||
|
- Efficient memory usage (streaming, not loading full file)
|
||||||
|
- Safari-compatible partial content responses
|
||||||
|
|
||||||
|
### 3. Caching Headers
|
||||||
|
All responses include cache headers:
|
||||||
|
```
|
||||||
|
Cache-Control: public, max-age=3600
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. S3 External Links
|
||||||
|
S3 files served via presigned URLs (no server download).
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Unit Tests (To Add)
|
||||||
|
See SAFARI_FIX.md for recommended test coverage.
|
||||||
|
|
||||||
|
### Manual Testing
|
||||||
|
```bash
|
||||||
|
# Test attachment
|
||||||
|
curl "http://localhost:8081/file/attachments/{uid}/file.jpg"
|
||||||
|
|
||||||
|
# Test avatar by ID
|
||||||
|
curl "http://localhost:8081/file/users/1/avatar"
|
||||||
|
|
||||||
|
# Test avatar by username
|
||||||
|
curl "http://localhost:8081/file/users/steven/avatar"
|
||||||
|
|
||||||
|
# Test range request
|
||||||
|
curl -H "Range: bytes=0-999" "http://localhost:8081/file/attachments/{uid}/video.mp4"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Improvements
|
||||||
|
|
||||||
|
See SAFARI_FIX.md section "Future Improvements" for planned enhancements.
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
- [SAFARI_FIX.md](../../../SAFARI_FIX.md) - Full migration guide
|
||||||
|
- [server/router/api/v1/auth.go](../api/v1/auth.go) - Auth constants source of truth
|
||||||
|
- [RFC 7233](https://tools.ietf.org/html/rfc7233) - HTTP Range Requests spec
|
||||||
|
|
@ -0,0 +1,569 @@
|
||||||
|
package fileserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/sync/semaphore"
|
||||||
|
|
||||||
|
"github.com/usememos/memos/internal/profile"
|
||||||
|
"github.com/usememos/memos/internal/util"
|
||||||
|
"github.com/usememos/memos/plugin/storage/s3"
|
||||||
|
storepb "github.com/usememos/memos/proto/gen/store"
|
||||||
|
apiv1 "github.com/usememos/memos/server/router/api/v1"
|
||||||
|
"github.com/usememos/memos/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ThumbnailCacheFolder is the folder name where the thumbnail images are stored.
|
||||||
|
ThumbnailCacheFolder = ".thumbnail_cache"
|
||||||
|
// thumbnailMaxSize is the maximum size in pixels for the largest dimension of the thumbnail image.
|
||||||
|
thumbnailMaxSize = 600
|
||||||
|
)
|
||||||
|
|
||||||
|
var SupportedThumbnailMimeTypes = []string{
|
||||||
|
"image/png",
|
||||||
|
"image/jpeg",
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileServerService handles HTTP file serving with proper range request support.
|
||||||
|
// This service bypasses gRPC-Gateway to use native HTTP serving via http.ServeContent(),
|
||||||
|
// which is required for Safari video/audio playback.
|
||||||
|
type FileServerService struct {
|
||||||
|
Profile *profile.Profile
|
||||||
|
Store *store.Store
|
||||||
|
Secret string
|
||||||
|
|
||||||
|
// thumbnailSemaphore limits concurrent thumbnail generation to prevent memory exhaustion
|
||||||
|
thumbnailSemaphore *semaphore.Weighted
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFileServerService creates a new file server service.
|
||||||
|
func NewFileServerService(profile *profile.Profile, store *store.Store, secret string) *FileServerService {
|
||||||
|
return &FileServerService{
|
||||||
|
Profile: profile,
|
||||||
|
Store: store,
|
||||||
|
Secret: secret,
|
||||||
|
thumbnailSemaphore: semaphore.NewWeighted(3), // Limit to 3 concurrent thumbnail generations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRoutes registers HTTP file serving routes.
|
||||||
|
func (s *FileServerService) RegisterRoutes(echoServer *echo.Echo) {
|
||||||
|
fileGroup := echoServer.Group("/file")
|
||||||
|
|
||||||
|
// Serve attachment binary files
|
||||||
|
fileGroup.GET("/attachments/:uid/:filename", s.serveAttachmentFile)
|
||||||
|
|
||||||
|
// Serve user avatar images
|
||||||
|
fileGroup.GET("/users/:identifier/avatar", s.serveUserAvatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveAttachmentFile serves attachment binary content using native HTTP.
|
||||||
|
// This properly handles range requests required by Safari for video/audio playback.
|
||||||
|
func (s *FileServerService) serveAttachmentFile(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
uid := c.Param("uid")
|
||||||
|
thumbnail := c.QueryParam("thumbnail") == "true"
|
||||||
|
|
||||||
|
// Get attachment from database
|
||||||
|
attachment, err := s.Store.GetAttachment(ctx, &store.FindAttachment{
|
||||||
|
UID: &uid,
|
||||||
|
GetBlob: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get attachment").SetInternal(err)
|
||||||
|
}
|
||||||
|
if attachment == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, "attachment not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permissions - verify memo visibility if attachment belongs to a memo
|
||||||
|
if err := s.checkAttachmentPermission(ctx, c, attachment); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the binary content
|
||||||
|
blob, err := s.getAttachmentBlob(attachment)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get attachment blob").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle thumbnail requests for images
|
||||||
|
if thumbnail && s.isImageType(attachment.Type) {
|
||||||
|
thumbnailBlob, err := s.getOrGenerateThumbnail(ctx, attachment)
|
||||||
|
if err != nil {
|
||||||
|
// Log warning but fall back to original image
|
||||||
|
c.Logger().Warnf("failed to get thumbnail: %v", err)
|
||||||
|
} else {
|
||||||
|
blob = thumbnailBlob
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine content type
|
||||||
|
contentType := attachment.Type
|
||||||
|
if strings.HasPrefix(contentType, "text/") {
|
||||||
|
contentType += "; charset=utf-8"
|
||||||
|
}
|
||||||
|
// Prevent XSS attacks by serving potentially unsafe files as octet-stream
|
||||||
|
if strings.EqualFold(contentType, "image/svg+xml") ||
|
||||||
|
strings.EqualFold(contentType, "text/html") ||
|
||||||
|
strings.EqualFold(contentType, "application/xhtml+xml") {
|
||||||
|
contentType = "application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set common headers
|
||||||
|
c.Response().Header().Set("Content-Type", contentType)
|
||||||
|
c.Response().Header().Set("Cache-Control", "public, max-age=3600")
|
||||||
|
|
||||||
|
// For video/audio: Use http.ServeContent for automatic range request support
|
||||||
|
// This is critical for Safari which REQUIRES range request support
|
||||||
|
if strings.HasPrefix(contentType, "video/") || strings.HasPrefix(contentType, "audio/") {
|
||||||
|
// ServeContent automatically handles:
|
||||||
|
// - Range request parsing
|
||||||
|
// - HTTP 206 Partial Content responses
|
||||||
|
// - Content-Range headers
|
||||||
|
// - Accept-Ranges: bytes header
|
||||||
|
modTime := time.Unix(attachment.UpdatedTs, 0)
|
||||||
|
http.ServeContent(c.Response(), c.Request(), attachment.Filename, modTime, bytes.NewReader(blob))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other files: Simple blob response
|
||||||
|
return c.Blob(http.StatusOK, contentType, blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveUserAvatar serves user avatar images.
|
||||||
|
// Supports both user ID and username as identifier.
|
||||||
|
func (s *FileServerService) serveUserAvatar(c echo.Context) error {
|
||||||
|
ctx := c.Request().Context()
|
||||||
|
identifier := c.Param("identifier")
|
||||||
|
|
||||||
|
// Try to find user by ID or username
|
||||||
|
user, err := s.getUserByIdentifier(ctx, identifier)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get user").SetInternal(err)
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, "user not found")
|
||||||
|
}
|
||||||
|
if user.AvatarURL == "" {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, "avatar not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract image info from data URI
|
||||||
|
imageType, base64Data, err := s.extractImageInfo(user.AvatarURL)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "failed to extract image info").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode base64 data
|
||||||
|
imageData, err := base64.StdEncoding.DecodeString(base64Data)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "failed to decode image data").SetInternal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set cache headers for avatars
|
||||||
|
c.Response().Header().Set("Content-Type", imageType)
|
||||||
|
c.Response().Header().Set("Cache-Control", "public, max-age=3600")
|
||||||
|
|
||||||
|
return c.Blob(http.StatusOK, imageType, imageData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUserByIdentifier finds a user by either ID or username.
|
||||||
|
func (s *FileServerService) getUserByIdentifier(ctx context.Context, identifier string) (*store.User, error) {
|
||||||
|
// Try to parse as ID first
|
||||||
|
if userID, err := util.ConvertStringToInt32(identifier); err == nil {
|
||||||
|
return s.Store.GetUser(ctx, &store.FindUser{ID: &userID})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, treat as username
|
||||||
|
return s.Store.GetUser(ctx, &store.FindUser{Username: &identifier})
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractImageInfo extracts image type and base64 data from a data URI.
|
||||||
|
// Data URI format: data:image/png;base64,iVBORw0KGgo...
|
||||||
|
func (*FileServerService) extractImageInfo(dataURI string) (string, string, error) {
|
||||||
|
dataURIRegex := regexp.MustCompile(`^data:(?P<type>.+);base64,(?P<base64>.+)`)
|
||||||
|
matches := dataURIRegex.FindStringSubmatch(dataURI)
|
||||||
|
if len(matches) != 3 {
|
||||||
|
return "", "", errors.New("invalid data URI format")
|
||||||
|
}
|
||||||
|
imageType := matches[1]
|
||||||
|
base64Data := matches[2]
|
||||||
|
return imageType, base64Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkAttachmentPermission verifies the user has permission to access the attachment.
|
||||||
|
func (s *FileServerService) checkAttachmentPermission(ctx context.Context, c echo.Context, attachment *store.Attachment) error {
|
||||||
|
// If attachment is not linked to a memo, allow access
|
||||||
|
if attachment.MemoID == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check memo visibility
|
||||||
|
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
|
||||||
|
ID: attachment.MemoID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "failed to find memo").SetInternal(err)
|
||||||
|
}
|
||||||
|
if memo == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusNotFound, "memo not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public memos are accessible to everyone
|
||||||
|
if memo.Visibility == store.Public {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-public memos, check authentication
|
||||||
|
user, err := s.getCurrentUser(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return echo.NewHTTPError(http.StatusInternalServerError, "failed to get current user").SetInternal(err)
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
return echo.NewHTTPError(http.StatusUnauthorized, "unauthorized access")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private memos can only be accessed by the creator
|
||||||
|
if memo.Visibility == store.Private && user.ID != attachment.CreatorID {
|
||||||
|
return echo.NewHTTPError(http.StatusForbidden, "forbidden access")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCurrentUser retrieves the current authenticated user from the Echo context.
|
||||||
|
// It checks both session cookies and Bearer tokens for authentication.
|
||||||
|
func (s *FileServerService) getCurrentUser(ctx context.Context, c echo.Context) (*store.User, error) {
|
||||||
|
// Try session cookie authentication first
|
||||||
|
if cookie, err := c.Cookie(apiv1.SessionCookieName); err == nil && cookie.Value != "" {
|
||||||
|
user, err := s.authenticateBySession(ctx, cookie.Value)
|
||||||
|
if err == nil && user != nil {
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try JWT Bearer token authentication
|
||||||
|
authHeader := c.Request().Header.Get("Authorization")
|
||||||
|
if authHeader != "" {
|
||||||
|
parts := strings.Fields(authHeader)
|
||||||
|
if len(parts) == 2 && strings.ToLower(parts[0]) == "bearer" {
|
||||||
|
user, err := s.authenticateByJWT(ctx, parts[1])
|
||||||
|
if err == nil && user != nil {
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No valid authentication found
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// authenticateBySession authenticates a user using session ID from cookie.
|
||||||
|
func (s *FileServerService) authenticateBySession(ctx context.Context, sessionCookieValue string) (*store.User, error) {
|
||||||
|
if sessionCookieValue == "" {
|
||||||
|
return nil, errors.New("session cookie value not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the cookie value to extract userID and sessionID
|
||||||
|
userID, sessionID, err := s.parseSessionCookieValue(sessionCookieValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "invalid session cookie format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the user
|
||||||
|
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||||
|
ID: &userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to get user")
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
return nil, errors.New("user not found")
|
||||||
|
}
|
||||||
|
if user.RowStatus == store.Archived {
|
||||||
|
return nil, errors.New("user is archived")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user sessions and validate the sessionID
|
||||||
|
sessions, err := s.Store.GetUserSessions(ctx, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to get user sessions")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.validateUserSession(sessionID, sessions) {
|
||||||
|
return nil, errors.New("invalid or expired session")
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// authenticateByJWT authenticates a user using JWT access token from Authorization header.
|
||||||
|
func (s *FileServerService) authenticateByJWT(ctx context.Context, accessToken string) (*store.User, error) {
|
||||||
|
if accessToken == "" {
|
||||||
|
return nil, errors.New("access token not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := &apiv1.ClaimsMessage{}
|
||||||
|
_, err := jwt.ParseWithClaims(accessToken, claims, func(t *jwt.Token) (any, error) {
|
||||||
|
if t.Method.Alg() != jwt.SigningMethodHS256.Name {
|
||||||
|
return nil, errors.Errorf("unexpected access token signing method=%v, expect %v", t.Header["alg"], jwt.SigningMethodHS256)
|
||||||
|
}
|
||||||
|
if kid, ok := t.Header["kid"].(string); ok {
|
||||||
|
if kid == apiv1.KeyID {
|
||||||
|
return []byte(s.Secret), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.Errorf("unexpected access token kid=%v", t.Header["kid"])
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Invalid or expired access token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user from JWT claims
|
||||||
|
userID, err := util.ConvertStringToInt32(claims.Subject)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "malformed ID in the token")
|
||||||
|
}
|
||||||
|
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||||
|
ID: &userID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to get user")
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
return nil, errors.Errorf("user %q not exists", userID)
|
||||||
|
}
|
||||||
|
if user.RowStatus == store.Archived {
|
||||||
|
return nil, errors.Errorf("user %q is archived", userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that this access token exists in the user's access tokens
|
||||||
|
accessTokens, err := s.Store.GetUserAccessTokens(ctx, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to get user access tokens")
|
||||||
|
}
|
||||||
|
if !s.validateAccessToken(accessToken, accessTokens) {
|
||||||
|
return nil, errors.New("invalid access token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSessionCookieValue parses the session cookie value to extract userID and sessionID.
|
||||||
|
func (*FileServerService) parseSessionCookieValue(cookieValue string) (int32, string, error) {
|
||||||
|
parts := strings.SplitN(cookieValue, "-", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return 0, "", errors.New("invalid session cookie format")
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := util.ConvertStringToInt32(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", errors.Errorf("invalid user ID in session cookie: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return userID, parts[1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateUserSession checks if a session exists and is still valid using sliding expiration.
|
||||||
|
func (*FileServerService) validateUserSession(sessionID string, userSessions []*storepb.SessionsUserSetting_Session) bool {
|
||||||
|
for _, session := range userSessions {
|
||||||
|
if sessionID == session.SessionId {
|
||||||
|
// Use sliding expiration: check if last_accessed_time + 14 days > current_time
|
||||||
|
if session.LastAccessedTime != nil {
|
||||||
|
expirationTime := session.LastAccessedTime.AsTime().Add(apiv1.SessionSlidingDuration)
|
||||||
|
if expirationTime.Before(time.Now()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateAccessToken checks if the provided JWT token exists in the user's access tokens list.
|
||||||
|
func (*FileServerService) validateAccessToken(accessTokenString string, userAccessTokens []*storepb.AccessTokensUserSetting_AccessToken) bool {
|
||||||
|
for _, userAccessToken := range userAccessTokens {
|
||||||
|
if accessTokenString == userAccessToken.AccessToken {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isImageType checks if the mime type is an image that supports thumbnails.
|
||||||
|
func (*FileServerService) isImageType(mimeType string) bool {
|
||||||
|
return mimeType == "image/png" || mimeType == "image/jpeg"
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAttachmentBlob retrieves the binary content of an attachment from storage.
|
||||||
|
func (s *FileServerService) getAttachmentBlob(attachment *store.Attachment) ([]byte, error) {
|
||||||
|
// For local storage, read the file from the local disk.
|
||||||
|
if attachment.StorageType == storepb.AttachmentStorageType_LOCAL {
|
||||||
|
attachmentPath := filepath.FromSlash(attachment.Reference)
|
||||||
|
if !filepath.IsAbs(attachmentPath) {
|
||||||
|
attachmentPath = filepath.Join(s.Profile.Data, attachmentPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(attachmentPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, errors.Wrap(err, "file not found")
|
||||||
|
}
|
||||||
|
return nil, errors.Wrap(err, "failed to open the file")
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
blob, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to read the file")
|
||||||
|
}
|
||||||
|
return blob, nil
|
||||||
|
}
|
||||||
|
// For S3 storage, download the file from S3.
|
||||||
|
if attachment.StorageType == storepb.AttachmentStorageType_S3 {
|
||||||
|
if attachment.Payload == nil {
|
||||||
|
return nil, errors.New("attachment payload is missing")
|
||||||
|
}
|
||||||
|
s3Object := attachment.Payload.GetS3Object()
|
||||||
|
if s3Object == nil {
|
||||||
|
return nil, errors.New("S3 object payload is missing")
|
||||||
|
}
|
||||||
|
if s3Object.S3Config == nil {
|
||||||
|
return nil, errors.New("S3 config is missing")
|
||||||
|
}
|
||||||
|
if s3Object.Key == "" {
|
||||||
|
return nil, errors.New("S3 object key is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
s3Client, err := s3.NewClient(context.Background(), s3Object.S3Config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to create S3 client")
|
||||||
|
}
|
||||||
|
|
||||||
|
blob, err := s3Client.GetObject(context.Background(), s3Object.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to get object from S3")
|
||||||
|
}
|
||||||
|
return blob, nil
|
||||||
|
}
|
||||||
|
// For database storage, return the blob from the database.
|
||||||
|
return attachment.Blob, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOrGenerateThumbnail returns the thumbnail image of the attachment.
|
||||||
|
// Uses semaphore to limit concurrent thumbnail generation and prevent memory exhaustion.
|
||||||
|
func (s *FileServerService) getOrGenerateThumbnail(ctx context.Context, attachment *store.Attachment) ([]byte, error) {
|
||||||
|
thumbnailCacheFolder := filepath.Join(s.Profile.Data, ThumbnailCacheFolder)
|
||||||
|
if err := os.MkdirAll(thumbnailCacheFolder, os.ModePerm); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to create thumbnail cache folder")
|
||||||
|
}
|
||||||
|
filePath := filepath.Join(thumbnailCacheFolder, fmt.Sprintf("%d%s", attachment.ID, filepath.Ext(attachment.Filename)))
|
||||||
|
|
||||||
|
// Check if thumbnail already exists
|
||||||
|
if _, err := os.Stat(filePath); err == nil {
|
||||||
|
// Thumbnail exists, read and return it
|
||||||
|
thumbnailFile, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to open thumbnail file")
|
||||||
|
}
|
||||||
|
defer thumbnailFile.Close()
|
||||||
|
blob, err := io.ReadAll(thumbnailFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to read thumbnail file")
|
||||||
|
}
|
||||||
|
return blob, nil
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return nil, errors.Wrap(err, "failed to check thumbnail image stat")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumbnail doesn't exist, acquire semaphore to limit concurrent generation
|
||||||
|
if err := s.thumbnailSemaphore.Acquire(ctx, 1); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to acquire thumbnail generation semaphore")
|
||||||
|
}
|
||||||
|
defer s.thumbnailSemaphore.Release(1)
|
||||||
|
|
||||||
|
// Double-check if thumbnail was created while waiting for semaphore
|
||||||
|
if _, err := os.Stat(filePath); err == nil {
|
||||||
|
thumbnailFile, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to open thumbnail file")
|
||||||
|
}
|
||||||
|
defer thumbnailFile.Close()
|
||||||
|
blob, err := io.ReadAll(thumbnailFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to read thumbnail file")
|
||||||
|
}
|
||||||
|
return blob, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the thumbnail
|
||||||
|
blob, err := s.getAttachmentBlob(attachment)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to get attachment blob")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode image - this is memory intensive
|
||||||
|
img, err := imaging.Decode(bytes.NewReader(blob), imaging.AutoOrientation(true))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to decode thumbnail image")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The largest dimension is set to thumbnailMaxSize and the smaller dimension is scaled proportionally.
|
||||||
|
// Small images are not enlarged.
|
||||||
|
width := img.Bounds().Dx()
|
||||||
|
height := img.Bounds().Dy()
|
||||||
|
var thumbnailWidth, thumbnailHeight int
|
||||||
|
|
||||||
|
// Only resize if the image is larger than thumbnailMaxSize
|
||||||
|
if max(width, height) > thumbnailMaxSize {
|
||||||
|
if width >= height {
|
||||||
|
// Landscape or square - constrain width, maintain aspect ratio for height
|
||||||
|
thumbnailWidth = thumbnailMaxSize
|
||||||
|
thumbnailHeight = 0
|
||||||
|
} else {
|
||||||
|
// Portrait - constrain height, maintain aspect ratio for width
|
||||||
|
thumbnailWidth = 0
|
||||||
|
thumbnailHeight = thumbnailMaxSize
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Keep original dimensions for small images
|
||||||
|
thumbnailWidth = width
|
||||||
|
thumbnailHeight = height
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize the image to the calculated dimensions.
|
||||||
|
thumbnailImage := imaging.Resize(img, thumbnailWidth, thumbnailHeight, imaging.Lanczos)
|
||||||
|
|
||||||
|
// Save thumbnail to disk
|
||||||
|
if err := imaging.Save(thumbnailImage, filePath); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to save thumbnail file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the saved thumbnail and return it
|
||||||
|
thumbnailFile, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to open thumbnail file")
|
||||||
|
}
|
||||||
|
defer thumbnailFile.Close()
|
||||||
|
thumbnailBlob, err := io.ReadAll(thumbnailFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to read thumbnail file")
|
||||||
|
}
|
||||||
|
return thumbnailBlob, nil
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"github.com/usememos/memos/internal/profile"
|
"github.com/usememos/memos/internal/profile"
|
||||||
storepb "github.com/usememos/memos/proto/gen/store"
|
storepb "github.com/usememos/memos/proto/gen/store"
|
||||||
apiv1 "github.com/usememos/memos/server/router/api/v1"
|
apiv1 "github.com/usememos/memos/server/router/api/v1"
|
||||||
|
"github.com/usememos/memos/server/router/fileserver"
|
||||||
"github.com/usememos/memos/server/router/frontend"
|
"github.com/usememos/memos/server/router/frontend"
|
||||||
"github.com/usememos/memos/server/router/rss"
|
"github.com/usememos/memos/server/router/rss"
|
||||||
"github.com/usememos/memos/server/runner/s3presign"
|
"github.com/usememos/memos/server/runner/s3presign"
|
||||||
|
|
@ -87,6 +88,11 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store
|
||||||
|
|
||||||
apiV1Service := apiv1.NewAPIV1Service(s.Secret, profile, store, grpcServer)
|
apiV1Service := apiv1.NewAPIV1Service(s.Secret, profile, store, grpcServer)
|
||||||
|
|
||||||
|
// Register HTTP file server routes BEFORE gRPC-Gateway to ensure proper range request handling for Safari.
|
||||||
|
// This uses native HTTP serving (http.ServeContent) instead of gRPC for video/audio files.
|
||||||
|
fileServerService := fileserver.NewFileServerService(s.Profile, s.Store, s.Secret)
|
||||||
|
fileServerService.RegisterRoutes(echoServer)
|
||||||
|
|
||||||
// Create and register RSS routes (needs markdown service from apiV1Service).
|
// Create and register RSS routes (needs markdown service from apiV1Service).
|
||||||
rss.NewRSSService(s.Profile, s.Store, apiV1Service.MarkdownService).RegisterRoutes(rootGroup)
|
rss.NewRSSService(s.Profile, s.Store, apiV1Service.MarkdownService).RegisterRoutes(rootGroup)
|
||||||
// Register gRPC gateway as api v1.
|
// Register gRPC gateway as api v1.
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||||
import { HttpBody } from "../../google/api/httpbody";
|
|
||||||
import { Empty } from "../../google/protobuf/empty";
|
import { Empty } from "../../google/protobuf/empty";
|
||||||
import { FieldMask } from "../../google/protobuf/field_mask";
|
import { FieldMask } from "../../google/protobuf/field_mask";
|
||||||
import { Timestamp } from "../../google/protobuf/timestamp";
|
import { Timestamp } from "../../google/protobuf/timestamp";
|
||||||
|
|
@ -99,18 +98,6 @@ export interface GetAttachmentRequest {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetAttachmentBinaryRequest {
|
|
||||||
/**
|
|
||||||
* Required. The attachment name of the attachment.
|
|
||||||
* Format: attachments/{attachment}
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
/** The filename of the attachment. Mainly used for downloading. */
|
|
||||||
filename: string;
|
|
||||||
/** Optional. A flag indicating if the thumbnail version of the attachment should be returned. */
|
|
||||||
thumbnail: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateAttachmentRequest {
|
export interface UpdateAttachmentRequest {
|
||||||
/** Required. The attachment which replaces the attachment on the server. */
|
/** Required. The attachment which replaces the attachment on the server. */
|
||||||
attachment?:
|
attachment?:
|
||||||
|
|
@ -525,76 +512,6 @@ export const GetAttachmentRequest: MessageFns<GetAttachmentRequest> = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function createBaseGetAttachmentBinaryRequest(): GetAttachmentBinaryRequest {
|
|
||||||
return { name: "", filename: "", thumbnail: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GetAttachmentBinaryRequest: MessageFns<GetAttachmentBinaryRequest> = {
|
|
||||||
encode(message: GetAttachmentBinaryRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
|
||||||
if (message.name !== "") {
|
|
||||||
writer.uint32(10).string(message.name);
|
|
||||||
}
|
|
||||||
if (message.filename !== "") {
|
|
||||||
writer.uint32(18).string(message.filename);
|
|
||||||
}
|
|
||||||
if (message.thumbnail !== false) {
|
|
||||||
writer.uint32(24).bool(message.thumbnail);
|
|
||||||
}
|
|
||||||
return writer;
|
|
||||||
},
|
|
||||||
|
|
||||||
decode(input: BinaryReader | Uint8Array, length?: number): GetAttachmentBinaryRequest {
|
|
||||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
|
||||||
const message = createBaseGetAttachmentBinaryRequest();
|
|
||||||
while (reader.pos < end) {
|
|
||||||
const tag = reader.uint32();
|
|
||||||
switch (tag >>> 3) {
|
|
||||||
case 1: {
|
|
||||||
if (tag !== 10) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.name = reader.string();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
case 2: {
|
|
||||||
if (tag !== 18) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.filename = reader.string();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
case 3: {
|
|
||||||
if (tag !== 24) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.thumbnail = reader.bool();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
reader.skip(tag & 7);
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
|
|
||||||
create(base?: DeepPartial<GetAttachmentBinaryRequest>): GetAttachmentBinaryRequest {
|
|
||||||
return GetAttachmentBinaryRequest.fromPartial(base ?? {});
|
|
||||||
},
|
|
||||||
fromPartial(object: DeepPartial<GetAttachmentBinaryRequest>): GetAttachmentBinaryRequest {
|
|
||||||
const message = createBaseGetAttachmentBinaryRequest();
|
|
||||||
message.name = object.name ?? "";
|
|
||||||
message.filename = object.filename ?? "";
|
|
||||||
message.thumbnail = object.thumbnail ?? false;
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function createBaseUpdateAttachmentRequest(): UpdateAttachmentRequest {
|
function createBaseUpdateAttachmentRequest(): UpdateAttachmentRequest {
|
||||||
return { attachment: undefined, updateMask: undefined };
|
return { attachment: undefined, updateMask: undefined };
|
||||||
}
|
}
|
||||||
|
|
@ -843,90 +760,6 @@ export const AttachmentServiceDefinition = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
/** GetAttachmentBinary returns a attachment binary by name. */
|
|
||||||
getAttachmentBinary: {
|
|
||||||
name: "GetAttachmentBinary",
|
|
||||||
requestType: GetAttachmentBinaryRequest,
|
|
||||||
requestStream: false,
|
|
||||||
responseType: HttpBody,
|
|
||||||
responseStream: false,
|
|
||||||
options: {
|
|
||||||
_unknownFields: {
|
|
||||||
8410: [
|
|
||||||
new Uint8Array([
|
|
||||||
23,
|
|
||||||
110,
|
|
||||||
97,
|
|
||||||
109,
|
|
||||||
101,
|
|
||||||
44,
|
|
||||||
102,
|
|
||||||
105,
|
|
||||||
108,
|
|
||||||
101,
|
|
||||||
110,
|
|
||||||
97,
|
|
||||||
109,
|
|
||||||
101,
|
|
||||||
44,
|
|
||||||
116,
|
|
||||||
104,
|
|
||||||
117,
|
|
||||||
109,
|
|
||||||
98,
|
|
||||||
110,
|
|
||||||
97,
|
|
||||||
105,
|
|
||||||
108,
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
578365826: [
|
|
||||||
new Uint8Array([
|
|
||||||
39,
|
|
||||||
18,
|
|
||||||
37,
|
|
||||||
47,
|
|
||||||
102,
|
|
||||||
105,
|
|
||||||
108,
|
|
||||||
101,
|
|
||||||
47,
|
|
||||||
123,
|
|
||||||
110,
|
|
||||||
97,
|
|
||||||
109,
|
|
||||||
101,
|
|
||||||
61,
|
|
||||||
97,
|
|
||||||
116,
|
|
||||||
116,
|
|
||||||
97,
|
|
||||||
99,
|
|
||||||
104,
|
|
||||||
109,
|
|
||||||
101,
|
|
||||||
110,
|
|
||||||
116,
|
|
||||||
115,
|
|
||||||
47,
|
|
||||||
42,
|
|
||||||
125,
|
|
||||||
47,
|
|
||||||
123,
|
|
||||||
102,
|
|
||||||
105,
|
|
||||||
108,
|
|
||||||
101,
|
|
||||||
110,
|
|
||||||
97,
|
|
||||||
109,
|
|
||||||
101,
|
|
||||||
125,
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
/** UpdateAttachment updates a attachment. */
|
/** UpdateAttachment updates a attachment. */
|
||||||
updateAttachment: {
|
updateAttachment: {
|
||||||
name: "UpdateAttachment",
|
name: "UpdateAttachment",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||||
import { HttpBody } from "../../google/api/httpbody";
|
|
||||||
import { Empty } from "../../google/protobuf/empty";
|
import { Empty } from "../../google/protobuf/empty";
|
||||||
import { FieldMask } from "../../google/protobuf/field_mask";
|
import { FieldMask } from "../../google/protobuf/field_mask";
|
||||||
import { Timestamp } from "../../google/protobuf/timestamp";
|
import { Timestamp } from "../../google/protobuf/timestamp";
|
||||||
|
|
@ -189,14 +188,6 @@ export interface DeleteUserRequest {
|
||||||
force: boolean;
|
force: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetUserAvatarRequest {
|
|
||||||
/**
|
|
||||||
* Required. The resource name of the user.
|
|
||||||
* Format: users/{user}
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** User statistics messages */
|
/** User statistics messages */
|
||||||
export interface UserStats {
|
export interface UserStats {
|
||||||
/**
|
/**
|
||||||
|
|
@ -1298,52 +1289,6 @@ export const DeleteUserRequest: MessageFns<DeleteUserRequest> = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function createBaseGetUserAvatarRequest(): GetUserAvatarRequest {
|
|
||||||
return { name: "" };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GetUserAvatarRequest: MessageFns<GetUserAvatarRequest> = {
|
|
||||||
encode(message: GetUserAvatarRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
|
||||||
if (message.name !== "") {
|
|
||||||
writer.uint32(10).string(message.name);
|
|
||||||
}
|
|
||||||
return writer;
|
|
||||||
},
|
|
||||||
|
|
||||||
decode(input: BinaryReader | Uint8Array, length?: number): GetUserAvatarRequest {
|
|
||||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
|
||||||
const message = createBaseGetUserAvatarRequest();
|
|
||||||
while (reader.pos < end) {
|
|
||||||
const tag = reader.uint32();
|
|
||||||
switch (tag >>> 3) {
|
|
||||||
case 1: {
|
|
||||||
if (tag !== 10) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.name = reader.string();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
reader.skip(tag & 7);
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
|
|
||||||
create(base?: DeepPartial<GetUserAvatarRequest>): GetUserAvatarRequest {
|
|
||||||
return GetUserAvatarRequest.fromPartial(base ?? {});
|
|
||||||
},
|
|
||||||
fromPartial(object: DeepPartial<GetUserAvatarRequest>): GetUserAvatarRequest {
|
|
||||||
const message = createBaseGetUserAvatarRequest();
|
|
||||||
message.name = object.name ?? "";
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function createBaseUserStats(): UserStats {
|
function createBaseUserStats(): UserStats {
|
||||||
return {
|
return {
|
||||||
name: "",
|
name: "",
|
||||||
|
|
@ -3885,55 +3830,6 @@ export const UserServiceDefinition = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
/** GetUserAvatar gets the avatar of a user. */
|
|
||||||
getUserAvatar: {
|
|
||||||
name: "GetUserAvatar",
|
|
||||||
requestType: GetUserAvatarRequest,
|
|
||||||
requestStream: false,
|
|
||||||
responseType: HttpBody,
|
|
||||||
responseStream: false,
|
|
||||||
options: {
|
|
||||||
_unknownFields: {
|
|
||||||
8410: [new Uint8Array([4, 110, 97, 109, 101])],
|
|
||||||
578365826: [
|
|
||||||
new Uint8Array([
|
|
||||||
31,
|
|
||||||
18,
|
|
||||||
29,
|
|
||||||
47,
|
|
||||||
97,
|
|
||||||
112,
|
|
||||||
105,
|
|
||||||
47,
|
|
||||||
118,
|
|
||||||
49,
|
|
||||||
47,
|
|
||||||
123,
|
|
||||||
110,
|
|
||||||
97,
|
|
||||||
109,
|
|
||||||
101,
|
|
||||||
61,
|
|
||||||
117,
|
|
||||||
115,
|
|
||||||
101,
|
|
||||||
114,
|
|
||||||
115,
|
|
||||||
47,
|
|
||||||
42,
|
|
||||||
125,
|
|
||||||
47,
|
|
||||||
97,
|
|
||||||
118,
|
|
||||||
97,
|
|
||||||
116,
|
|
||||||
97,
|
|
||||||
114,
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
/** ListAllUserStats returns statistics for all users. */
|
/** ListAllUserStats returns statistics for all users. */
|
||||||
listAllUserStats: {
|
listAllUserStats: {
|
||||||
name: "ListAllUserStats",
|
name: "ListAllUserStats",
|
||||||
|
|
|
||||||
|
|
@ -1,152 +0,0 @@
|
||||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// protoc-gen-ts_proto v2.6.1
|
|
||||||
// protoc unknown
|
|
||||||
// source: google/api/httpbody.proto
|
|
||||||
|
|
||||||
/* eslint-disable */
|
|
||||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
|
||||||
import { Any } from "../protobuf/any";
|
|
||||||
|
|
||||||
export const protobufPackage = "google.api";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Message that represents an arbitrary HTTP body. It should only be used for
|
|
||||||
* payload formats that can't be represented as JSON, such as raw binary or
|
|
||||||
* an HTML page.
|
|
||||||
*
|
|
||||||
* This message can be used both in streaming and non-streaming API methods in
|
|
||||||
* the request as well as the response.
|
|
||||||
*
|
|
||||||
* It can be used as a top-level request field, which is convenient if one
|
|
||||||
* wants to extract parameters from either the URL or HTTP template into the
|
|
||||||
* request fields and also want access to the raw HTTP body.
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
*
|
|
||||||
* message GetResourceRequest {
|
|
||||||
* // A unique request id.
|
|
||||||
* string request_id = 1;
|
|
||||||
*
|
|
||||||
* // The raw HTTP body is bound to this field.
|
|
||||||
* google.api.HttpBody http_body = 2;
|
|
||||||
*
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* service ResourceService {
|
|
||||||
* rpc GetResource(GetResourceRequest)
|
|
||||||
* returns (google.api.HttpBody);
|
|
||||||
* rpc UpdateResource(google.api.HttpBody)
|
|
||||||
* returns (google.protobuf.Empty);
|
|
||||||
*
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Example with streaming methods:
|
|
||||||
*
|
|
||||||
* service CaldavService {
|
|
||||||
* rpc GetCalendar(stream google.api.HttpBody)
|
|
||||||
* returns (stream google.api.HttpBody);
|
|
||||||
* rpc UpdateCalendar(stream google.api.HttpBody)
|
|
||||||
* returns (stream google.api.HttpBody);
|
|
||||||
*
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Use of this type only changes how the request and response bodies are
|
|
||||||
* handled, all other features will continue to work unchanged.
|
|
||||||
*/
|
|
||||||
export interface HttpBody {
|
|
||||||
/** The HTTP Content-Type header value specifying the content type of the body. */
|
|
||||||
contentType: string;
|
|
||||||
/** The HTTP request/response body as raw binary. */
|
|
||||||
data: Uint8Array;
|
|
||||||
/**
|
|
||||||
* Application specific response metadata. Must be set in the first response
|
|
||||||
* for streaming APIs.
|
|
||||||
*/
|
|
||||||
extensions: Any[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function createBaseHttpBody(): HttpBody {
|
|
||||||
return { contentType: "", data: new Uint8Array(0), extensions: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HttpBody: MessageFns<HttpBody> = {
|
|
||||||
encode(message: HttpBody, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
|
||||||
if (message.contentType !== "") {
|
|
||||||
writer.uint32(10).string(message.contentType);
|
|
||||||
}
|
|
||||||
if (message.data.length !== 0) {
|
|
||||||
writer.uint32(18).bytes(message.data);
|
|
||||||
}
|
|
||||||
for (const v of message.extensions) {
|
|
||||||
Any.encode(v!, writer.uint32(26).fork()).join();
|
|
||||||
}
|
|
||||||
return writer;
|
|
||||||
},
|
|
||||||
|
|
||||||
decode(input: BinaryReader | Uint8Array, length?: number): HttpBody {
|
|
||||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
|
||||||
const message = createBaseHttpBody();
|
|
||||||
while (reader.pos < end) {
|
|
||||||
const tag = reader.uint32();
|
|
||||||
switch (tag >>> 3) {
|
|
||||||
case 1: {
|
|
||||||
if (tag !== 10) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.contentType = reader.string();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
case 2: {
|
|
||||||
if (tag !== 18) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.data = reader.bytes();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
case 3: {
|
|
||||||
if (tag !== 26) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.extensions.push(Any.decode(reader, reader.uint32()));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
reader.skip(tag & 7);
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
|
|
||||||
create(base?: DeepPartial<HttpBody>): HttpBody {
|
|
||||||
return HttpBody.fromPartial(base ?? {});
|
|
||||||
},
|
|
||||||
fromPartial(object: DeepPartial<HttpBody>): HttpBody {
|
|
||||||
const message = createBaseHttpBody();
|
|
||||||
message.contentType = object.contentType ?? "";
|
|
||||||
message.data = object.data ?? new Uint8Array(0);
|
|
||||||
message.extensions = object.extensions?.map((e) => Any.fromPartial(e)) || [];
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
|
||||||
|
|
||||||
export type DeepPartial<T> = T extends Builtin ? T
|
|
||||||
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
|
||||||
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
|
||||||
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
|
||||||
: Partial<T>;
|
|
||||||
|
|
||||||
export interface MessageFns<T> {
|
|
||||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
|
||||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
|
||||||
create(base?: DeepPartial<T>): T;
|
|
||||||
fromPartial(object: DeepPartial<T>): T;
|
|
||||||
}
|
|
||||||
|
|
@ -1,206 +0,0 @@
|
||||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// protoc-gen-ts_proto v2.6.1
|
|
||||||
// protoc unknown
|
|
||||||
// source: google/protobuf/any.proto
|
|
||||||
|
|
||||||
/* eslint-disable */
|
|
||||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
|
||||||
|
|
||||||
export const protobufPackage = "google.protobuf";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* `Any` contains an arbitrary serialized protocol buffer message along with a
|
|
||||||
* URL that describes the type of the serialized message.
|
|
||||||
*
|
|
||||||
* Protobuf library provides support to pack/unpack Any values in the form
|
|
||||||
* of utility functions or additional generated methods of the Any type.
|
|
||||||
*
|
|
||||||
* Example 1: Pack and unpack a message in C++.
|
|
||||||
*
|
|
||||||
* Foo foo = ...;
|
|
||||||
* Any any;
|
|
||||||
* any.PackFrom(foo);
|
|
||||||
* ...
|
|
||||||
* if (any.UnpackTo(&foo)) {
|
|
||||||
* ...
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Example 2: Pack and unpack a message in Java.
|
|
||||||
*
|
|
||||||
* Foo foo = ...;
|
|
||||||
* Any any = Any.pack(foo);
|
|
||||||
* ...
|
|
||||||
* if (any.is(Foo.class)) {
|
|
||||||
* foo = any.unpack(Foo.class);
|
|
||||||
* }
|
|
||||||
* // or ...
|
|
||||||
* if (any.isSameTypeAs(Foo.getDefaultInstance())) {
|
|
||||||
* foo = any.unpack(Foo.getDefaultInstance());
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Example 3: Pack and unpack a message in Python.
|
|
||||||
*
|
|
||||||
* foo = Foo(...)
|
|
||||||
* any = Any()
|
|
||||||
* any.Pack(foo)
|
|
||||||
* ...
|
|
||||||
* if any.Is(Foo.DESCRIPTOR):
|
|
||||||
* any.Unpack(foo)
|
|
||||||
* ...
|
|
||||||
*
|
|
||||||
* Example 4: Pack and unpack a message in Go
|
|
||||||
*
|
|
||||||
* foo := &pb.Foo{...}
|
|
||||||
* any, err := anypb.New(foo)
|
|
||||||
* if err != nil {
|
|
||||||
* ...
|
|
||||||
* }
|
|
||||||
* ...
|
|
||||||
* foo := &pb.Foo{}
|
|
||||||
* if err := any.UnmarshalTo(foo); err != nil {
|
|
||||||
* ...
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* The pack methods provided by protobuf library will by default use
|
|
||||||
* 'type.googleapis.com/full.type.name' as the type URL and the unpack
|
|
||||||
* methods only use the fully qualified type name after the last '/'
|
|
||||||
* in the type URL, for example "foo.bar.com/x/y.z" will yield type
|
|
||||||
* name "y.z".
|
|
||||||
*
|
|
||||||
* JSON
|
|
||||||
* ====
|
|
||||||
* The JSON representation of an `Any` value uses the regular
|
|
||||||
* representation of the deserialized, embedded message, with an
|
|
||||||
* additional field `@type` which contains the type URL. Example:
|
|
||||||
*
|
|
||||||
* package google.profile;
|
|
||||||
* message Person {
|
|
||||||
* string first_name = 1;
|
|
||||||
* string last_name = 2;
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* "@type": "type.googleapis.com/google.profile.Person",
|
|
||||||
* "firstName": <string>,
|
|
||||||
* "lastName": <string>
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* If the embedded message type is well-known and has a custom JSON
|
|
||||||
* representation, that representation will be embedded adding a field
|
|
||||||
* `value` which holds the custom JSON in addition to the `@type`
|
|
||||||
* field. Example (for message [google.protobuf.Duration][]):
|
|
||||||
*
|
|
||||||
* {
|
|
||||||
* "@type": "type.googleapis.com/google.protobuf.Duration",
|
|
||||||
* "value": "1.212s"
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
export interface Any {
|
|
||||||
/**
|
|
||||||
* A URL/resource name that uniquely identifies the type of the serialized
|
|
||||||
* protocol buffer message. This string must contain at least
|
|
||||||
* one "/" character. The last segment of the URL's path must represent
|
|
||||||
* the fully qualified name of the type (as in
|
|
||||||
* `path/google.protobuf.Duration`). The name should be in a canonical form
|
|
||||||
* (e.g., leading "." is not accepted).
|
|
||||||
*
|
|
||||||
* In practice, teams usually precompile into the binary all types that they
|
|
||||||
* expect it to use in the context of Any. However, for URLs which use the
|
|
||||||
* scheme `http`, `https`, or no scheme, one can optionally set up a type
|
|
||||||
* server that maps type URLs to message definitions as follows:
|
|
||||||
*
|
|
||||||
* * If no scheme is provided, `https` is assumed.
|
|
||||||
* * An HTTP GET on the URL must yield a [google.protobuf.Type][]
|
|
||||||
* value in binary format, or produce an error.
|
|
||||||
* * Applications are allowed to cache lookup results based on the
|
|
||||||
* URL, or have them precompiled into a binary to avoid any
|
|
||||||
* lookup. Therefore, binary compatibility needs to be preserved
|
|
||||||
* on changes to types. (Use versioned type names to manage
|
|
||||||
* breaking changes.)
|
|
||||||
*
|
|
||||||
* Note: this functionality is not currently available in the official
|
|
||||||
* protobuf release, and it is not used for type URLs beginning with
|
|
||||||
* type.googleapis.com. As of May 2023, there are no widely used type server
|
|
||||||
* implementations and no plans to implement one.
|
|
||||||
*
|
|
||||||
* Schemes other than `http`, `https` (or the empty scheme) might be
|
|
||||||
* used with implementation specific semantics.
|
|
||||||
*/
|
|
||||||
typeUrl: string;
|
|
||||||
/** Must be a valid serialized protocol buffer of the above specified type. */
|
|
||||||
value: Uint8Array;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createBaseAny(): Any {
|
|
||||||
return { typeUrl: "", value: new Uint8Array(0) };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Any: MessageFns<Any> = {
|
|
||||||
encode(message: Any, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
|
||||||
if (message.typeUrl !== "") {
|
|
||||||
writer.uint32(10).string(message.typeUrl);
|
|
||||||
}
|
|
||||||
if (message.value.length !== 0) {
|
|
||||||
writer.uint32(18).bytes(message.value);
|
|
||||||
}
|
|
||||||
return writer;
|
|
||||||
},
|
|
||||||
|
|
||||||
decode(input: BinaryReader | Uint8Array, length?: number): Any {
|
|
||||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
|
||||||
let end = length === undefined ? reader.len : reader.pos + length;
|
|
||||||
const message = createBaseAny();
|
|
||||||
while (reader.pos < end) {
|
|
||||||
const tag = reader.uint32();
|
|
||||||
switch (tag >>> 3) {
|
|
||||||
case 1: {
|
|
||||||
if (tag !== 10) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.typeUrl = reader.string();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
case 2: {
|
|
||||||
if (tag !== 18) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.value = reader.bytes();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
reader.skip(tag & 7);
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
|
|
||||||
create(base?: DeepPartial<Any>): Any {
|
|
||||||
return Any.fromPartial(base ?? {});
|
|
||||||
},
|
|
||||||
fromPartial(object: DeepPartial<Any>): Any {
|
|
||||||
const message = createBaseAny();
|
|
||||||
message.typeUrl = object.typeUrl ?? "";
|
|
||||||
message.value = object.value ?? new Uint8Array(0);
|
|
||||||
return message;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
|
||||||
|
|
||||||
export type DeepPartial<T> = T extends Builtin ? T
|
|
||||||
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
|
||||||
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
|
||||||
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
|
||||||
: Partial<T>;
|
|
||||||
|
|
||||||
export interface MessageFns<T> {
|
|
||||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
|
||||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
|
||||||
create(base?: DeepPartial<T>): T;
|
|
||||||
fromPartial(object: DeepPartial<T>): T;
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue