Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions proto/api/v1/workspace_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 17 additions & 6 deletions proto/gen/api/v1/workspace_service.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions proto/gen/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3345,6 +3345,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
Expand Down
21 changes: 16 additions & 5 deletions proto/gen/store/workspace_setting.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions proto/store/workspace_setting.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
33 changes: 23 additions & 10 deletions server/router/api/v1/attachment_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}

Expand Down
34 changes: 26 additions & 8 deletions server/router/api/v1/workspace_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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{
Expand All @@ -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{
Expand Down
12 changes: 12 additions & 0 deletions web/src/components/Settings/StorageSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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")}
Expand Down
1 change: 1 addition & 0 deletions web/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,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",
Expand Down
1 change: 1 addition & 0 deletions web/src/store/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 21 additions & 1 deletion web/src/types/proto/api/v1/workspace_service.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions web/src/utils/attachment.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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`;
};

Expand Down