diff --git a/proto/api/v1/workspace_service.proto b/proto/api/v1/workspace_service.proto index fc30aee2a..0764bf70c 100644 --- a/proto/api/v1/workspace_service.proto +++ b/proto/api/v1/workspace_service.proto @@ -147,6 +147,9 @@ message WorkspaceSetting { } // The S3 config. S3Config s3_config = 4; + // enable_s3_image_thumbnails enables thumbnail generation for images stored in S3. + // When false, images stored in S3 will not have thumbnails generated. + bool enable_s3_image_thumbnails = 5; } // Memo-related workspace settings and policies. diff --git a/proto/gen/api/v1/workspace_service.pb.go b/proto/gen/api/v1/workspace_service.pb.go index e1e9ef4a0..da1467a65 100644 --- a/proto/gen/api/v1/workspace_service.pb.go +++ b/proto/gen/api/v1/workspace_service.pb.go @@ -589,9 +589,12 @@ type WorkspaceSetting_StorageSetting struct { // The max upload size in megabytes. UploadSizeLimitMb int64 `protobuf:"varint,3,opt,name=upload_size_limit_mb,json=uploadSizeLimitMb,proto3" json:"upload_size_limit_mb,omitempty"` // The S3 config. - S3Config *WorkspaceSetting_StorageSetting_S3Config `protobuf:"bytes,4,opt,name=s3_config,json=s3Config,proto3" json:"s3_config,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + S3Config *WorkspaceSetting_StorageSetting_S3Config `protobuf:"bytes,4,opt,name=s3_config,json=s3Config,proto3" json:"s3_config,omitempty"` + // enable_s3_image_thumbnails enables thumbnail generation for images stored in S3. + // When false, images stored in S3 will not have thumbnails generated. + EnableS3ImageThumbnails bool `protobuf:"varint,5,opt,name=enable_s3_image_thumbnails,json=enableS3ImageThumbnails,proto3" json:"enable_s3_image_thumbnails,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *WorkspaceSetting_StorageSetting) Reset() { @@ -652,6 +655,13 @@ func (x *WorkspaceSetting_StorageSetting) GetS3Config() *WorkspaceSetting_Storag return nil } +func (x *WorkspaceSetting_StorageSetting) GetEnableS3ImageThumbnails() bool { + if x != nil { + return x.EnableS3ImageThumbnails + } + return false +} + // Memo-related workspace settings and policies. type WorkspaceSetting_MemoRelatedSetting struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -935,7 +945,7 @@ const file_api_v1_workspace_service_proto_rawDesc = "" + "\aversion\x18\x02 \x01(\tR\aversion\x12\x12\n" + "\x04mode\x18\x03 \x01(\tR\x04mode\x12!\n" + "\finstance_url\x18\x06 \x01(\tR\vinstanceUrl\"\x1c\n" + - "\x1aGetWorkspaceProfileRequest\"\x97\x11\n" + + "\x1aGetWorkspaceProfileRequest\"\xd4\x11\n" + "\x10WorkspaceSetting\x12\x17\n" + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12X\n" + "\x0fgeneral_setting\x18\x02 \x01(\v2-.memos.api.v1.WorkspaceSetting.GeneralSettingH\x00R\x0egeneralSetting\x12X\n" + @@ -955,12 +965,13 @@ const file_api_v1_workspace_service_proto_rawDesc = "" + "\x05title\x18\x01 \x01(\tR\x05title\x12 \n" + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" + "\blogo_url\x18\x03 \x01(\tR\alogoUrl\x12\x16\n" + - "\x06locale\x18\x04 \x01(\tR\x06locale\x1a\xbe\x04\n" + + "\x06locale\x18\x04 \x01(\tR\x06locale\x1a\xfb\x04\n" + "\x0eStorageSetting\x12\\\n" + "\fstorage_type\x18\x01 \x01(\x0e29.memos.api.v1.WorkspaceSetting.StorageSetting.StorageTypeR\vstorageType\x12+\n" + "\x11filepath_template\x18\x02 \x01(\tR\x10filepathTemplate\x12/\n" + "\x14upload_size_limit_mb\x18\x03 \x01(\x03R\x11uploadSizeLimitMb\x12S\n" + - "\ts3_config\x18\x04 \x01(\v26.memos.api.v1.WorkspaceSetting.StorageSetting.S3ConfigR\bs3Config\x1a\xcc\x01\n" + + "\ts3_config\x18\x04 \x01(\v26.memos.api.v1.WorkspaceSetting.StorageSetting.S3ConfigR\bs3Config\x12;\n" + + "\x1aenable_s3_image_thumbnails\x18\x05 \x01(\bR\x17enableS3ImageThumbnails\x1a\xcc\x01\n" + "\bS3Config\x12\"\n" + "\raccess_key_id\x18\x01 \x01(\tR\vaccessKeyId\x12*\n" + "\x11access_key_secret\x18\x02 \x01(\tR\x0faccessKeySecret\x12\x1a\n" + diff --git a/proto/gen/openapi.yaml b/proto/gen/openapi.yaml index c9dfc653c..cd107b04e 100644 --- a/proto/gen/openapi.yaml +++ b/proto/gen/openapi.yaml @@ -3188,6 +3188,11 @@ components: allOf: - $ref: '#/components/schemas/StorageSetting_S3Config' description: The S3 config. + enableS3ImageThumbnails: + type: boolean + description: |- + enable_s3_image_thumbnails enables thumbnail generation for images stored in S3. + When false, images stored in S3 will not have thumbnails generated. description: Storage configuration settings for workspace attachments. tags: - name: ActivityService diff --git a/proto/gen/store/workspace_setting.pb.go b/proto/gen/store/workspace_setting.pb.go index 5c09483f5..42096636e 100644 --- a/proto/gen/store/workspace_setting.pb.go +++ b/proto/gen/store/workspace_setting.pb.go @@ -509,9 +509,12 @@ type WorkspaceStorageSetting struct { // The max upload size in megabytes. UploadSizeLimitMb int64 `protobuf:"varint,3,opt,name=upload_size_limit_mb,json=uploadSizeLimitMb,proto3" json:"upload_size_limit_mb,omitempty"` // The S3 config. - S3Config *StorageS3Config `protobuf:"bytes,4,opt,name=s3_config,json=s3Config,proto3" json:"s3_config,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + S3Config *StorageS3Config `protobuf:"bytes,4,opt,name=s3_config,json=s3Config,proto3" json:"s3_config,omitempty"` + // enable_s3_image_thumbnails enables thumbnail generation for images stored in S3. + // When false, images stored in S3 will not have thumbnails generated. + EnableS3ImageThumbnails bool `protobuf:"varint,5,opt,name=enable_s3_image_thumbnails,json=enableS3ImageThumbnails,proto3" json:"enable_s3_image_thumbnails,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *WorkspaceStorageSetting) Reset() { @@ -572,6 +575,13 @@ func (x *WorkspaceStorageSetting) GetS3Config() *StorageS3Config { return nil } +func (x *WorkspaceStorageSetting) GetEnableS3ImageThumbnails() bool { + if x != nil { + return x.EnableS3ImageThumbnails + } + return false +} + // Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/ type StorageS3Config struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -804,12 +814,13 @@ const file_store_workspace_setting_proto_rawDesc = "" + "\x05title\x18\x01 \x01(\tR\x05title\x12 \n" + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" + "\blogo_url\x18\x03 \x01(\tR\alogoUrl\x12\x16\n" + - "\x06locale\x18\x04 \x01(\tR\x06locale\"\xd5\x02\n" + + "\x06locale\x18\x04 \x01(\tR\x06locale\"\x92\x03\n" + "\x17WorkspaceStorageSetting\x12S\n" + "\fstorage_type\x18\x01 \x01(\x0e20.memos.store.WorkspaceStorageSetting.StorageTypeR\vstorageType\x12+\n" + "\x11filepath_template\x18\x02 \x01(\tR\x10filepathTemplate\x12/\n" + "\x14upload_size_limit_mb\x18\x03 \x01(\x03R\x11uploadSizeLimitMb\x129\n" + - "\ts3_config\x18\x04 \x01(\v2\x1c.memos.store.StorageS3ConfigR\bs3Config\"L\n" + + "\ts3_config\x18\x04 \x01(\v2\x1c.memos.store.StorageS3ConfigR\bs3Config\x12;\n" + + "\x1aenable_s3_image_thumbnails\x18\x05 \x01(\bR\x17enableS3ImageThumbnails\"L\n" + "\vStorageType\x12\x1c\n" + "\x18STORAGE_TYPE_UNSPECIFIED\x10\x00\x12\f\n" + "\bDATABASE\x10\x01\x12\t\n" + diff --git a/proto/store/workspace_setting.proto b/proto/store/workspace_setting.proto index eb86aba5f..adfc69835 100644 --- a/proto/store/workspace_setting.proto +++ b/proto/store/workspace_setting.proto @@ -83,6 +83,9 @@ message WorkspaceStorageSetting { int64 upload_size_limit_mb = 3; // The S3 config. StorageS3Config s3_config = 4; + // enable_s3_image_thumbnails enables thumbnail generation for images stored in S3. + // When false, images stored in S3 will not have thumbnails generated. + bool enable_s3_image_thumbnails = 5; } // Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/ diff --git a/server/router/api/v1/attachment_service.go b/server/router/api/v1/attachment_service.go index 64f342904..71e0e3b92 100644 --- a/server/router/api/v1/attachment_service.go +++ b/server/router/api/v1/attachment_service.go @@ -232,16 +232,29 @@ func (s *APIV1Service) GetAttachmentBinary(ctx context.Context, request *v1pb.Ge } if request.Thumbnail && util.HasPrefixes(attachment.Type, SupportedThumbnailMimeTypes...) { - thumbnailBlob, err := s.getOrGenerateThumbnail(attachment) - if err != nil { - // thumbnail failures are logged as warnings and not cosidered critical failures as - // a 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 + // Check if we should generate thumbnails for S3 images + shouldGenerateThumbnail := true + if attachment.StorageType == storepb.AttachmentStorageType_S3 { + storageSetting, err := s.Store.GetWorkspaceStorageSetting(ctx) + if err != nil { + slog.Warn("failed to get workspace storage setting", slog.Any("error", err)) + } else if !storageSetting.EnableS3ImageThumbnails { + shouldGenerateThumbnail = false + } + } + + if shouldGenerateThumbnail { + thumbnailBlob, err := s.getOrGenerateThumbnail(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 + } } } diff --git a/server/router/api/v1/workspace_service.go b/server/router/api/v1/workspace_service.go index 0af794c62..69e952797 100644 --- a/server/router/api/v1/workspace_service.go +++ b/server/router/api/v1/workspace_service.go @@ -64,14 +64,30 @@ func (s *APIV1Service) GetWorkspaceSetting(ctx context.Context, request *v1pb.Ge return nil, status.Errorf(codes.NotFound, "workspace setting not found") } - // For storage setting, only host can get it. + // For storage setting, filter based on user role. if workspaceSetting.Key == storepb.WorkspaceSettingKey_STORAGE { user, err := s.GetCurrentUser(ctx) if err != nil { return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) } + + // Host can see everything, regular users only see enable_s3_image_thumbnails. if user == nil || user.Role != store.RoleHost { - return nil, status.Errorf(codes.PermissionDenied, "permission denied") + // Convert and filter for non-host users. + convertedSetting := convertWorkspaceStorageSettingFromStore(workspaceSetting.GetStorageSetting()) + // Clear sensitive fields. + convertedSetting.StorageType = v1pb.WorkspaceSetting_StorageSetting_STORAGE_TYPE_UNSPECIFIED + convertedSetting.FilepathTemplate = "" + convertedSetting.UploadSizeLimitMb = 0 + convertedSetting.S3Config = nil + // Keep only EnableS3ImageThumbnails. + + return &v1pb.WorkspaceSetting{ + Name: fmt.Sprintf("workspace/settings/%s", workspaceSetting.Key.String()), + Value: &v1pb.WorkspaceSetting_StorageSetting_{ + StorageSetting: convertedSetting, + }, + }, nil } } @@ -211,9 +227,10 @@ func convertWorkspaceStorageSettingFromStore(settingpb *storepb.WorkspaceStorage return nil } setting := &v1pb.WorkspaceSetting_StorageSetting{ - StorageType: v1pb.WorkspaceSetting_StorageSetting_StorageType(settingpb.StorageType), - FilepathTemplate: settingpb.FilepathTemplate, - UploadSizeLimitMb: settingpb.UploadSizeLimitMb, + StorageType: v1pb.WorkspaceSetting_StorageSetting_StorageType(settingpb.StorageType), + FilepathTemplate: settingpb.FilepathTemplate, + UploadSizeLimitMb: settingpb.UploadSizeLimitMb, + EnableS3ImageThumbnails: settingpb.EnableS3ImageThumbnails, } if settingpb.S3Config != nil { setting.S3Config = &v1pb.WorkspaceSetting_StorageSetting_S3Config{ @@ -233,9 +250,10 @@ func convertWorkspaceStorageSettingToStore(setting *v1pb.WorkspaceSetting_Storag return nil } settingpb := &storepb.WorkspaceStorageSetting{ - StorageType: storepb.WorkspaceStorageSetting_StorageType(setting.StorageType), - FilepathTemplate: setting.FilepathTemplate, - UploadSizeLimitMb: setting.UploadSizeLimitMb, + StorageType: storepb.WorkspaceStorageSetting_StorageType(setting.StorageType), + FilepathTemplate: setting.FilepathTemplate, + UploadSizeLimitMb: setting.UploadSizeLimitMb, + EnableS3ImageThumbnails: setting.EnableS3ImageThumbnails, } if setting.S3Config != nil { settingpb.S3Config = &storepb.StorageS3Config{ diff --git a/web/src/components/Settings/StorageSection.tsx b/web/src/components/Settings/StorageSection.tsx index 8216cb8b1..b10b3f093 100644 --- a/web/src/components/Settings/StorageSection.tsx +++ b/web/src/components/Settings/StorageSection.tsx @@ -240,6 +240,18 @@ const StorageSection = observer(() => { )} +
+ {t("setting.storage-section.use-thumbnails-for-s3-images")} + + setWorkspaceStorageSetting({ + ...workspaceStorageSetting, + enableS3ImageThumbnails: checked, + }) + } + /> +