Florian Dewald 2 days ago committed by GitHub
commit 24d533a490
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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.

@ -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" +

@ -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

@ -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" +

@ -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/

@ -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
}
}
}

@ -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{

@ -240,6 +240,18 @@ const StorageSection = observer(() => {
</div>
</>
)}
<div className="w-full flex flex-row justify-between items-center">
<span>{t("setting.storage-section.use-thumbnails-for-s3-images")}</span>
<Switch
checked={workspaceStorageSetting.enableS3ImageThumbnails}
onCheckedChange={(checked) =>
setWorkspaceStorageSetting({
...workspaceStorageSetting,
enableS3ImageThumbnails: checked,
})
}
/>
</div>
<div>
<Button disabled={!allowSaveStorageSetting} onClick={saveWorkspaceStorageSetting}>
{t("common.save")}

@ -390,6 +390,7 @@
"url-prefix-placeholder": "Custom URL prefix, optional",
"url-suffix": "URL suffix",
"url-suffix-placeholder": "Custom URL suffix, optional",
"use-thumbnails-for-s3-images": "Generate and serve thumbnails for images stored in S3",
"warning-text": "Are you sure you want to delete storage service `{{name}}`? THIS ACTION IS IRREVERSIBLE"
},
"system": "System",

@ -247,6 +247,7 @@ export const initialWorkspaceStore = async (): Promise<void> => {
await Promise.all([
workspaceStore.fetchWorkspaceSetting(WorkspaceSetting_Key.GENERAL),
workspaceStore.fetchWorkspaceSetting(WorkspaceSetting_Key.MEMO_RELATED),
workspaceStore.fetchWorkspaceSetting(WorkspaceSetting_Key.STORAGE),
]);
// Apply settings to state

@ -141,7 +141,14 @@ export interface WorkspaceSetting_StorageSetting {
/** The max upload size in megabytes. */
uploadSizeLimitMb: number;
/** The S3 config. */
s3Config?: WorkspaceSetting_StorageSetting_S3Config | undefined;
s3Config?:
| WorkspaceSetting_StorageSetting_S3Config
| undefined;
/**
* enable_s3_image_thumbnails enables thumbnail generation for images stored in S3.
* When false, images stored in S3 will not have thumbnails generated.
*/
enableS3ImageThumbnails: boolean;
}
/** Storage type enumeration for different storage backends. */
@ -705,6 +712,7 @@ function createBaseWorkspaceSetting_StorageSetting(): WorkspaceSetting_StorageSe
filepathTemplate: "",
uploadSizeLimitMb: 0,
s3Config: undefined,
enableS3ImageThumbnails: false,
};
}
@ -722,6 +730,9 @@ export const WorkspaceSetting_StorageSetting: MessageFns<WorkspaceSetting_Storag
if (message.s3Config !== undefined) {
WorkspaceSetting_StorageSetting_S3Config.encode(message.s3Config, writer.uint32(34).fork()).join();
}
if (message.enableS3ImageThumbnails !== false) {
writer.uint32(40).bool(message.enableS3ImageThumbnails);
}
return writer;
},
@ -764,6 +775,14 @@ export const WorkspaceSetting_StorageSetting: MessageFns<WorkspaceSetting_Storag
message.s3Config = WorkspaceSetting_StorageSetting_S3Config.decode(reader, reader.uint32());
continue;
}
case 5: {
if (tag !== 40) {
break;
}
message.enableS3ImageThumbnails = reader.bool();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
@ -784,6 +803,7 @@ export const WorkspaceSetting_StorageSetting: MessageFns<WorkspaceSetting_Storag
message.s3Config = (object.s3Config !== undefined && object.s3Config !== null)
? WorkspaceSetting_StorageSetting_S3Config.fromPartial(object.s3Config)
: undefined;
message.enableS3ImageThumbnails = object.enableS3ImageThumbnails ?? false;
return message;
},
};

@ -1,4 +1,6 @@
import workspaceStore from "@/store/workspace";
import { Attachment } from "@/types/proto/api/v1/attachment_service";
import { WorkspaceSetting_Key } from "@/types/proto/api/v1/workspace_service";
export const getAttachmentUrl = (attachment: Attachment) => {
if (attachment.externalLink) {
@ -9,6 +11,14 @@ export const getAttachmentUrl = (attachment: Attachment) => {
};
export const getAttachmentThumbnailUrl = (attachment: Attachment) => {
// Don't request thumbnails for S3 images if the setting is disabled
if (
attachment.externalLink &&
!(workspaceStore.getWorkspaceSettingByKey(WorkspaceSetting_Key.STORAGE).storageSetting?.enableS3ImageThumbnails ?? false)
) {
return getAttachmentUrl(attachment);
}
return `${window.location.origin}/file/${attachment.name}/${attachment.filename}?thumbnail=true`;
};

Loading…
Cancel
Save