Introduce settings for thumbnail and image downsizing

pull/5184/head
Florian Dewald 1 week ago
parent d5cde36a58
commit 0ede2e8410

@ -147,6 +147,16 @@ message WorkspaceSetting {
}
// The S3 config.
S3Config s3_config = 4;
// The maximum size in pixels for the largest dimension of thumbnail images.
int32 thumbnail_max_size = 5;
// The JPEG quality (0-100) used when downscaling uploaded images.
int32 jpeg_quality = 6;
// The JPEG quality (0-100) used when generating thumbnails.
int32 thumbnail_jpeg_quality = 7;
// The maximum size in pixels for the largest dimension when storing images.
// Images larger than this will be downscaled before storage.
// Set to 0 to disable downscaling.
int32 image_max_size = 8;
}
// Memo-related workspace settings and policies.

@ -428,7 +428,6 @@ type GetUserRequest struct {
// Supports both numeric IDs and username strings:
// - users/{id} (e.g., users/101)
// - users/{username} (e.g., users/steven)
//
// Format: users/{id_or_username}
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
// Optional. The fields to return in the response.

@ -589,7 +589,17 @@ 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"`
S3Config *WorkspaceSetting_StorageSetting_S3Config `protobuf:"bytes,4,opt,name=s3_config,json=s3Config,proto3" json:"s3_config,omitempty"`
// The maximum size in pixels for the largest dimension of thumbnail images.
ThumbnailMaxSize int32 `protobuf:"varint,5,opt,name=thumbnail_max_size,json=thumbnailMaxSize,proto3" json:"thumbnail_max_size,omitempty"`
// The JPEG quality (0-100) used when downscaling uploaded images.
JpegQuality int32 `protobuf:"varint,6,opt,name=jpeg_quality,json=jpegQuality,proto3" json:"jpeg_quality,omitempty"`
// The JPEG quality (0-100) used when generating thumbnails.
ThumbnailJpegQuality int32 `protobuf:"varint,7,opt,name=thumbnail_jpeg_quality,json=thumbnailJpegQuality,proto3" json:"thumbnail_jpeg_quality,omitempty"`
// The maximum size in pixels for the largest dimension when storing images.
// Images larger than this will be downscaled before storage.
// Set to 0 to disable downscaling.
ImageMaxSize int32 `protobuf:"varint,8,opt,name=image_max_size,json=imageMaxSize,proto3" json:"image_max_size,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -652,6 +662,34 @@ func (x *WorkspaceSetting_StorageSetting) GetS3Config() *WorkspaceSetting_Storag
return nil
}
func (x *WorkspaceSetting_StorageSetting) GetThumbnailMaxSize() int32 {
if x != nil {
return x.ThumbnailMaxSize
}
return 0
}
func (x *WorkspaceSetting_StorageSetting) GetJpegQuality() int32 {
if x != nil {
return x.JpegQuality
}
return 0
}
func (x *WorkspaceSetting_StorageSetting) GetThumbnailJpegQuality() int32 {
if x != nil {
return x.ThumbnailJpegQuality
}
return 0
}
func (x *WorkspaceSetting_StorageSetting) GetImageMaxSize() int32 {
if x != nil {
return x.ImageMaxSize
}
return 0
}
// Memo-related workspace settings and policies.
type WorkspaceSetting_MemoRelatedSetting struct {
state protoimpl.MessageState `protogen:"open.v1"`
@ -935,7 +973,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\"\xc4\x12\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 +993,16 @@ 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\xeb\x05\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" +
"\x12thumbnail_max_size\x18\x05 \x01(\x05R\x10thumbnailMaxSize\x12!\n" +
"\fjpeg_quality\x18\x06 \x01(\x05R\vjpegQuality\x124\n" +
"\x16thumbnail_jpeg_quality\x18\a \x01(\x05R\x14thumbnailJpegQuality\x12$\n" +
"\x0eimage_max_size\x18\b \x01(\x05R\fimageMaxSize\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" +

@ -15,19 +15,13 @@ paths:
parameters:
- name: pageSize
in: query
description: |-
The maximum number of activities to return.
The service may return fewer than this value.
If unspecified, at most 100 activities will be returned.
The maximum value is 1000; values above 1000 will be coerced to 1000.
description: "The maximum number of activities to return.\r\n The service may return fewer than this value.\r\n If unspecified, at most 100 activities will be returned.\r\n The maximum value is 1000; values above 1000 will be coerced to 1000."
schema:
type: integer
format: int32
- name: pageToken
in: query
description: |-
A page token, received from a previous `ListActivities` call.
Provide this to retrieve the subsequent page.
description: "A page token, received from a previous `ListActivities` call.\r\n Provide this to retrieve the subsequent page."
schema:
type: string
responses:
@ -78,35 +72,23 @@ paths:
parameters:
- name: pageSize
in: query
description: |-
Optional. The maximum number of attachments to return.
The service may return fewer than this value.
If unspecified, at most 50 attachments will be returned.
The maximum value is 1000; values above 1000 will be coerced to 1000.
description: "Optional. The maximum number of attachments to return.\r\n The service may return fewer than this value.\r\n If unspecified, at most 50 attachments will be returned.\r\n The maximum value is 1000; values above 1000 will be coerced to 1000."
schema:
type: integer
format: int32
- name: pageToken
in: query
description: |-
Optional. A page token, received from a previous `ListAttachments` call.
Provide this to retrieve the subsequent page.
description: "Optional. A page token, received from a previous `ListAttachments` call.\r\n Provide this to retrieve the subsequent page."
schema:
type: string
- name: filter
in: query
description: |-
Optional. Filter to apply to the list results.
Example: "type=image/png" or "filename:*.jpg"
Supported operators: =, !=, <, <=, >, >=, :
Supported fields: filename, type, size, create_time, memo
description: "Optional. Filter to apply to the list results.\r\n Example: \"type=image/png\" or \"filename:*.jpg\"\r\n Supported operators: =, !=, <, <=, >, >=, :\r\n Supported fields: filename, type, size, create_time, memo"
schema:
type: string
- name: orderBy
in: query
description: |-
Optional. The order to sort results by.
Example: "create_time desc" or "filename asc"
description: "Optional. The order to sort results by.\r\n Example: \"create_time desc\" or \"filename asc\""
schema:
type: string
responses:
@ -130,9 +112,7 @@ paths:
parameters:
- name: attachmentId
in: query
description: |-
Optional. The attachment ID to use for this attachment.
If empty, a unique ID will be generated.
description: "Optional. The attachment ID to use for this attachment.\r\n If empty, a unique ID will be generated."
schema:
type: string
requestBody:
@ -243,9 +223,7 @@ paths:
post:
tags:
- AuthService
description: |-
CreateSession authenticates a user and creates a new session.
Returns the authenticated user information upon successful authentication.
description: "CreateSession authenticates a user and creates a new session.\r\n Returns the authenticated user information upon successful authentication."
operationId: AuthService_CreateSession
requestBody:
content:
@ -270,9 +248,7 @@ paths:
get:
tags:
- AuthService
description: |-
GetCurrentSession returns the current active session information.
This method is idempotent and safe, suitable for checking current session state.
description: "GetCurrentSession returns the current active session information.\r\n This method is idempotent and safe, suitable for checking current session state."
operationId: AuthService_GetCurrentSession
responses:
"200":
@ -290,9 +266,7 @@ paths:
delete:
tags:
- AuthService
description: |-
DeleteSession terminates the current user session.
This is an idempotent operation that invalidates the user's authentication.
description: "DeleteSession terminates the current user session.\r\n This is an idempotent operation that invalidates the user's authentication."
operationId: AuthService_DeleteSession
responses:
"200":
@ -331,9 +305,7 @@ paths:
parameters:
- name: identityProviderId
in: query
description: |-
Optional. The ID to use for the identity provider, which will become the final component of the resource name.
If not provided, the system will generate one.
description: "Optional. The ID to use for the identity provider, which will become the final component of the resource name.\r\n If not provided, the system will generate one."
schema:
type: string
requestBody:
@ -417,9 +389,7 @@ paths:
type: string
- name: updateMask
in: query
description: |-
Required. The update mask applies to the resource. Only the top level fields of
IdentityProvider are supported.
description: "Required. The update mask applies to the resource. Only the top level fields of\r\n IdentityProvider are supported."
schema:
type: string
format: field-mask
@ -511,9 +481,7 @@ paths:
get:
tags:
- MarkdownService
description: |-
GetLinkMetadata returns metadata for a given link.
This is useful for generating link previews.
description: "GetLinkMetadata returns metadata for a given link.\r\n This is useful for generating link previews."
operationId: MarkdownService_GetLinkMetadata
parameters:
- name: link
@ -538,9 +506,7 @@ paths:
post:
tags:
- MarkdownService
description: |-
ParseMarkdown parses the given markdown content and returns a list of nodes.
This is a utility method that transforms markdown text into structured nodes.
description: "ParseMarkdown parses the given markdown content and returns a list of nodes.\r\n This is a utility method that transforms markdown text into structured nodes."
operationId: MarkdownService_ParseMarkdown
requestBody:
content:
@ -565,9 +531,7 @@ paths:
post:
tags:
- MarkdownService
description: |-
RestoreMarkdownNodes restores the given nodes to markdown content.
This is the inverse operation of ParseMarkdown.
description: "RestoreMarkdownNodes restores the given nodes to markdown content.\r\n This is the inverse operation of ParseMarkdown."
operationId: MarkdownService_RestoreMarkdownNodes
requestBody:
content:
@ -592,9 +556,7 @@ paths:
post:
tags:
- MarkdownService
description: |-
StringifyMarkdownNodes stringify the given nodes to plain text content.
This removes all markdown formatting and returns plain text.
description: "StringifyMarkdownNodes stringify the given nodes to plain text content.\r\n This removes all markdown formatting and returns plain text."
operationId: MarkdownService_StringifyMarkdownNodes
requestBody:
content:
@ -624,26 +586,18 @@ paths:
parameters:
- name: pageSize
in: query
description: |-
Optional. The maximum number of memos to return.
The service may return fewer than this value.
If unspecified, at most 50 memos will be returned.
The maximum value is 1000; values above 1000 will be coerced to 1000.
description: "Optional. The maximum number of memos to return.\r\n The service may return fewer than this value.\r\n If unspecified, at most 50 memos will be returned.\r\n The maximum value is 1000; values above 1000 will be coerced to 1000."
schema:
type: integer
format: int32
- name: pageToken
in: query
description: |-
Optional. A page token, received from a previous `ListMemos` call.
Provide this to retrieve the subsequent page.
description: "Optional. A page token, received from a previous `ListMemos` call.\r\n Provide this to retrieve the subsequent page."
schema:
type: string
- name: state
in: query
description: |-
Optional. The state of the memos to list.
Default to `NORMAL`. Set to `ARCHIVED` to list archived memos.
description: "Optional. The state of the memos to list.\r\n Default to `NORMAL`. Set to `ARCHIVED` to list archived memos."
schema:
enum:
- STATE_UNSPECIFIED
@ -653,20 +607,12 @@ paths:
format: enum
- name: orderBy
in: query
description: |-
Optional. The order to sort results by.
Default to "display_time desc".
Supports comma-separated list of fields following AIP-132.
Example: "pinned desc, display_time desc" or "create_time asc"
Supported fields: pinned, display_time, create_time, update_time, name
description: "Optional. The order to sort results by.\r\n Default to \"display_time desc\".\r\n Supports comma-separated list of fields following AIP-132.\r\n Example: \"pinned desc, display_time desc\" or \"create_time asc\"\r\n Supported fields: pinned, display_time, create_time, update_time, name"
schema:
type: string
- name: filter
in: query
description: |-
Optional. Filter to apply to the list results.
Filter is a CEL expression to filter memos.
Refer to `Shortcut.filter`.
description: "Optional. Filter to apply to the list results.\r\n Filter is a CEL expression to filter memos.\r\n Refer to `Shortcut.filter`."
schema:
type: string
- name: showDeleted
@ -695,9 +641,7 @@ paths:
parameters:
- name: memoId
in: query
description: |-
Optional. The memo ID to use for this memo.
If empty, a unique ID will be generated.
description: "Optional. The memo ID to use for this memo.\r\n If empty, a unique ID will be generated."
schema:
type: string
- name: validateOnly
@ -744,9 +688,7 @@ paths:
type: string
- name: readMask
in: query
description: |-
Optional. The fields to return in the response.
If not specified, all fields are returned.
description: "Optional. The fields to return in the response.\r\n If not specified, all fields are returned."
schema:
type: string
format: field-mask
@ -1198,28 +1140,18 @@ paths:
parameters:
- name: pageSize
in: query
description: |-
Optional. The maximum number of users to return.
The service may return fewer than this value.
If unspecified, at most 50 users will be returned.
The maximum value is 1000; values above 1000 will be coerced to 1000.
description: "Optional. The maximum number of users to return.\r\n The service may return fewer than this value.\r\n If unspecified, at most 50 users will be returned.\r\n The maximum value is 1000; values above 1000 will be coerced to 1000."
schema:
type: integer
format: int32
- name: pageToken
in: query
description: |-
Optional. A page token, received from a previous `ListUsers` call.
Provide this to retrieve the subsequent page.
description: "Optional. A page token, received from a previous `ListUsers` call.\r\n Provide this to retrieve the subsequent page."
schema:
type: string
- name: filter
in: query
description: |-
Optional. Filter to apply to the list results.
Example: "username == 'steven'"
Supported operators: ==
Supported fields: username
description: "Optional. Filter to apply to the list results.\r\n Example: \"username == 'steven'\"\r\n Supported operators: ==\r\n Supported fields: username"
schema:
type: string
- name: showDeleted
@ -1248,10 +1180,7 @@ paths:
parameters:
- name: userId
in: query
description: |-
Optional. The user ID to use for this user.
If empty, a unique ID will be generated.
Must match the pattern [a-z0-9-]+
description: "Optional. The user ID to use for this user.\r\n If empty, a unique ID will be generated.\r\n Must match the pattern [a-z0-9-]+"
schema:
type: string
- name: validateOnly
@ -1261,9 +1190,7 @@ paths:
type: boolean
- name: requestId
in: query
description: |-
Optional. An idempotency token that can be used to ensure that multiple
requests to create a user have the same result.
description: "Optional. An idempotency token that can be used to ensure that multiple\r\n requests to create a user have the same result."
schema:
type: string
requestBody:
@ -1289,11 +1216,7 @@ paths:
get:
tags:
- UserService
description: |-
GetUser gets a user by ID or username.
Supports both numeric IDs and username strings:
- users/{id} (e.g., users/101)
- users/{username} (e.g., users/steven)
description: "GetUser gets a user by ID or username.\r\n Supports both numeric IDs and username strings:\r\n - users/{id} (e.g., users/101)\r\n - users/{username} (e.g., users/steven)"
operationId: UserService_GetUser
parameters:
- name: user
@ -1304,9 +1227,7 @@ paths:
type: string
- name: readMask
in: query
description: |-
Optional. The fields to return in the response.
If not specified, all fields are returned.
description: "Optional. The fields to return in the response.\r\n If not specified, all fields are returned."
schema:
type: string
format: field-mask
@ -1533,35 +1454,23 @@ paths:
type: string
- name: pageSize
in: query
description: |-
Optional. The maximum number of inboxes to return.
The service may return fewer than this value.
If unspecified, at most 50 inboxes will be returned.
The maximum value is 1000; values above 1000 will be coerced to 1000.
description: "Optional. The maximum number of inboxes to return.\r\n The service may return fewer than this value.\r\n If unspecified, at most 50 inboxes will be returned.\r\n The maximum value is 1000; values above 1000 will be coerced to 1000."
schema:
type: integer
format: int32
- name: pageToken
in: query
description: |-
Optional. A page token, received from a previous `ListInboxes` call.
Provide this to retrieve the subsequent page.
description: "Optional. A page token, received from a previous `ListInboxes` call.\r\n Provide this to retrieve the subsequent page."
schema:
type: string
- name: filter
in: query
description: |-
Optional. Filter to apply to the list results.
Example: "status=UNREAD" or "type=MEMO_COMMENT"
Supported operators: =, !=
Supported fields: status, type, sender, create_time
description: "Optional. Filter to apply to the list results.\r\n Example: \"status=UNREAD\" or \"type=MEMO_COMMENT\"\r\n Supported operators: =, !=\r\n Supported fields: status, type, sender, create_time"
schema:
type: string
- name: orderBy
in: query
description: |-
Optional. The order to sort results by.
Example: "create_time desc" or "status asc"
description: "Optional. The order to sort results by.\r\n Example: \"create_time desc\" or \"status asc\""
schema:
type: string
responses:
@ -1647,19 +1556,13 @@ paths:
type: string
- name: pageSize
in: query
description: |-
Optional. The maximum number of settings to return.
The service may return fewer than this value.
If unspecified, at most 50 settings will be returned.
The maximum value is 1000; values above 1000 will be coerced to 1000.
description: "Optional. The maximum number of settings to return.\r\n The service may return fewer than this value.\r\n If unspecified, at most 50 settings will be returned.\r\n The maximum value is 1000; values above 1000 will be coerced to 1000."
schema:
type: integer
format: int32
- name: pageToken
in: query
description: |-
Optional. A page token, received from a previous `ListUserSettings` call.
Provide this to retrieve the subsequent page.
description: "Optional. A page token, received from a previous `ListUserSettings` call.\r\n Provide this to retrieve the subsequent page."
schema:
type: string
responses:
@ -2214,15 +2117,11 @@ components:
name:
readOnly: true
type: string
description: |-
The name of the activity.
Format: activities/{id}
description: "The name of the activity.\r\n Format: activities/{id}"
creator:
readOnly: true
type: string
description: |-
The name of the creator.
Format: users/{user}
description: "The name of the creator.\r\n Format: users/{user}"
type:
readOnly: true
enum:
@ -2257,14 +2156,10 @@ components:
properties:
memo:
type: string
description: |-
The memo name of comment.
Format: memos/{memo}
description: "The memo name of comment.\r\n Format: memos/{memo}"
relatedMemo:
type: string
description: |-
The name of related memo.
Format: memos/{memo}
description: "The name of related memo.\r\n Format: memos/{memo}"
description: ActivityMemoCommentPayload represents the payload of a memo comment activity.
ActivityPayload:
type: object
@ -2281,9 +2176,7 @@ components:
properties:
name:
type: string
description: |-
The name of the attachment.
Format: attachments/{attachment}
description: "The name of the attachment.\r\n Format: attachments/{attachment}"
createTime:
readOnly: true
type: string
@ -2309,9 +2202,7 @@ components:
description: Output only. The size of the attachment in bytes.
memo:
type: string
description: |-
Optional. The related memo. Refer to `Memo.name`.
Format: memos/{memo}
description: "Optional. The related memo. Refer to `Memo.name`.\r\n Format: memos/{memo}"
AutoLinkNode:
type: object
properties:
@ -2373,14 +2264,10 @@ components:
properties:
username:
type: string
description: |-
The username to sign in with.
Required field for password-based authentication.
description: "The username to sign in with.\r\n Required field for password-based authentication."
password:
type: string
description: |-
The password to sign in with.
Required field for password-based authentication.
description: "The password to sign in with.\r\n Required field for password-based authentication."
description: Nested message for password-based authentication credentials.
CreateSessionRequest_SSOCredentials:
required:
@ -2391,20 +2278,14 @@ components:
properties:
idpId:
type: integer
description: |-
The ID of the SSO provider.
Required field to identify the SSO provider.
description: "The ID of the SSO provider.\r\n Required field to identify the SSO provider."
format: int32
code:
type: string
description: |-
The authorization code from the SSO provider.
Required field for completing the SSO flow.
description: "The authorization code from the SSO provider.\r\n Required field for completing the SSO flow."
redirectUri:
type: string
description: |-
The redirect URI used in the SSO flow.
Required field for security validation.
description: "The redirect URI used in the SSO flow.\r\n Required field for security validation."
description: Nested message for SSO authentication credentials.
CreateSessionResponse:
type: object
@ -2415,9 +2296,7 @@ components:
description: The authenticated user information.
lastAccessedAt:
type: string
description: |-
Last time the session was accessed.
Used for sliding expiration calculation (last_accessed_time + 2 weeks).
description: "Last time the session was accessed.\r\n Used for sliding expiration calculation (last_accessed_time + 2 weeks)."
format: date-time
DeleteMemoTagRequest:
required:
@ -2427,9 +2306,7 @@ components:
properties:
parent:
type: string
description: |-
Required. The parent, who owns the tags.
Format: memos/{memo}. Use "memos/-" to delete all tags.
description: "Required. The parent, who owns the tags.\r\n Format: memos/{memo}. Use \"memos/-\" to delete all tags."
tag:
type: string
description: Required. The tag name to delete.
@ -2480,9 +2357,7 @@ components:
$ref: '#/components/schemas/User'
lastAccessedAt:
type: string
description: |-
Last time the session was accessed.
Used for sliding expiration calculation (last_accessed_time + 2 weeks).
description: "Last time the session was accessed.\r\n Used for sliding expiration calculation (last_accessed_time + 2 weeks)."
format: date-time
GoogleProtobufAny:
type: object
@ -2536,9 +2411,7 @@ components:
properties:
name:
type: string
description: |-
The resource name of the identity provider.
Format: identityProviders/{idp}
description: "The resource name of the identity provider.\r\n Format: identityProviders/{idp}"
type:
enum:
- TYPE_UNSPECIFIED
@ -2573,21 +2446,15 @@ components:
properties:
name:
type: string
description: |-
The resource name of the inbox.
Format: inboxes/{inbox}
description: "The resource name of the inbox.\r\n Format: inboxes/{inbox}"
sender:
readOnly: true
type: string
description: |-
The sender of the inbox notification.
Format: users/{user}
description: "The sender of the inbox notification.\r\n Format: users/{user}"
receiver:
readOnly: true
type: string
description: |-
The receiver of the inbox notification.
Format: users/{user}
description: "The receiver of the inbox notification.\r\n Format: users/{user}"
status:
enum:
- STATUS_UNSPECIFIED
@ -2657,10 +2524,7 @@ components:
description: The activities.
nextPageToken:
type: string
description: |-
A token to retrieve the next page of results.
Pass this value in the page_token field in the subsequent call to `ListActivities`
method to retrieve the next page of results.
description: "A token to retrieve the next page of results.\r\n Pass this value in the page_token field in the subsequent call to `ListActivities`\r\n method to retrieve the next page of results."
ListAllUserStatsResponse:
type: object
properties:
@ -2679,9 +2543,7 @@ components:
description: The list of attachments.
nextPageToken:
type: string
description: |-
A token that can be sent as `page_token` to retrieve the next page.
If this field is omitted, there are no subsequent pages.
description: "A token that can be sent as `page_token` to retrieve the next page.\r\n If this field is omitted, there are no subsequent pages."
totalSize:
type: integer
description: The total count of attachments (may be approximate).
@ -2704,9 +2566,7 @@ components:
description: The list of inboxes.
nextPageToken:
type: string
description: |-
A token that can be sent as `page_token` to retrieve the next page.
If this field is omitted, there are no subsequent pages.
description: "A token that can be sent as `page_token` to retrieve the next page.\r\n If this field is omitted, there are no subsequent pages."
totalSize:
type: integer
description: The total count of inboxes (may be approximate).
@ -2781,9 +2641,7 @@ components:
description: The list of memos.
nextPageToken:
type: string
description: |-
A token that can be sent as `page_token` to retrieve the next page.
If this field is omitted, there are no subsequent pages.
description: "A token that can be sent as `page_token` to retrieve the next page.\r\n If this field is omitted, there are no subsequent pages."
totalSize:
type: integer
description: The total count of memos (may be approximate).
@ -2847,9 +2705,7 @@ components:
description: The list of user settings.
nextPageToken:
type: string
description: |-
A token that can be sent as `page_token` to retrieve the next page.
If this field is omitted, there are no subsequent pages.
description: "A token that can be sent as `page_token` to retrieve the next page.\r\n If this field is omitted, there are no subsequent pages."
totalSize:
type: integer
description: The total count of settings (may be approximate).
@ -2873,9 +2729,7 @@ components:
description: The list of users.
nextPageToken:
type: string
description: |-
A token that can be sent as `page_token` to retrieve the next page.
If this field is omitted, there are no subsequent pages.
description: "A token that can be sent as `page_token` to retrieve the next page.\r\n If this field is omitted, there are no subsequent pages."
totalSize:
type: integer
description: The total count of users (may be approximate).
@ -2913,9 +2767,7 @@ components:
properties:
name:
type: string
description: |-
The resource name of the memo.
Format: memos/{memo}, memo is the user defined id or uuid.
description: "The resource name of the memo.\r\n Format: memos/{memo}, memo is the user defined id or uuid."
state:
enum:
- STATE_UNSPECIFIED
@ -2927,9 +2779,7 @@ components:
creator:
readOnly: true
type: string
description: |-
The name of the creator.
Format: users/{user}
description: "The name of the creator.\r\n Format: users/{user}"
createTime:
readOnly: true
type: string
@ -2995,9 +2845,7 @@ components:
parent:
readOnly: true
type: string
description: |-
Output only. The name of the parent memo.
Format: memos/{memo}
description: "Output only. The name of the parent memo.\r\n Format: memos/{memo}"
snippet:
readOnly: true
type: string
@ -3035,9 +2883,7 @@ components:
properties:
name:
type: string
description: |-
The resource name of the memo.
Format: memos/{memo}
description: "The resource name of the memo.\r\n Format: memos/{memo}"
snippet:
readOnly: true
type: string
@ -3223,21 +3069,14 @@ components:
name:
readOnly: true
type: string
description: |-
The resource name of the reaction.
Format: reactions/{reaction}
description: "The resource name of the reaction.\r\n Format: reactions/{reaction}"
creator:
readOnly: true
type: string
description: |-
The resource name of the creator.
Format: users/{user}
description: "The resource name of the creator.\r\n Format: users/{user}"
contentId:
type: string
description: |-
The resource name of the content.
For memo reactions, this should be the memo's resource name.
Format: memos/{memo}
description: "The resource name of the content.\r\n For memo reactions, this should be the memo's resource name.\r\n Format: memos/{memo}"
reactionType:
type: string
description: "Required. The type of reaction (e.g., \"\U0001F44D\", \"❤️\", \"\U0001F604\")."
@ -3264,9 +3103,7 @@ components:
properties:
parent:
type: string
description: |-
Required. The parent, who owns the tags.
Format: memos/{memo}. Use "memos/-" to rename all tags.
description: "Required. The parent, who owns the tags.\r\n Format: memos/{memo}. Use \"memos/-\" to rename all tags."
oldTag:
type: string
description: Required. The old tag name to rename.
@ -3297,9 +3134,7 @@ components:
properties:
name:
type: string
description: |-
Required. The resource name of the memo.
Format: memos/{memo}
description: "Required. The resource name of the memo.\r\n Format: memos/{memo}"
attachments:
type: array
items:
@ -3313,9 +3148,7 @@ components:
properties:
name:
type: string
description: |-
Required. The resource name of the memo.
Format: memos/{memo}
description: "Required. The resource name of the memo.\r\n Format: memos/{memo}"
relations:
type: array
items:
@ -3328,9 +3161,7 @@ components:
properties:
name:
type: string
description: |-
The resource name of the shortcut.
Format: users/{user}/shortcuts/{shortcut}
description: "The resource name of the shortcut.\r\n Format: users/{user}/shortcuts/{shortcut}"
title:
type: string
description: The title of the shortcut.
@ -3373,9 +3204,7 @@ components:
type: string
usePathStyle:
type: boolean
description: |-
S3 configuration for cloud storage backend.
Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/
description: "S3 configuration for cloud storage backend.\r\n Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/"
StrikethroughNode:
type: object
properties:
@ -3473,9 +3302,7 @@ components:
properties:
name:
type: string
description: |-
Required. The resource name of the memo.
Format: memos/{memo}
description: "Required. The resource name of the memo.\r\n Format: memos/{memo}"
reaction:
allOf:
- $ref: '#/components/schemas/Reaction'
@ -3489,9 +3316,7 @@ components:
properties:
name:
type: string
description: |-
The resource name of the user.
Format: users/{user}
description: "The resource name of the user.\r\n Format: users/{user}"
role:
enum:
- ROLE_UNSPECIFIED
@ -3543,9 +3368,7 @@ components:
properties:
name:
type: string
description: |-
The resource name of the access token.
Format: users/{user}/accessTokens/{access_token}
description: "The resource name of the access token.\r\n Format: users/{user}/accessTokens/{access_token}"
accessToken:
readOnly: true
type: string
@ -3568,9 +3391,7 @@ components:
properties:
name:
type: string
description: |-
The resource name of the session.
Format: users/{user}/sessions/{session}
description: "The resource name of the session.\r\n Format: users/{user}/sessions/{session}"
sessionId:
readOnly: true
type: string
@ -3583,9 +3404,7 @@ components:
lastAccessedTime:
readOnly: true
type: string
description: |-
The timestamp when the session was last accessed.
Used for sliding expiration calculation (last_accessed_time + 2 weeks).
description: "The timestamp when the session was last accessed.\r\n Used for sliding expiration calculation (last_accessed_time + 2 weeks)."
format: date-time
clientInfo:
readOnly: true
@ -3615,10 +3434,7 @@ components:
properties:
name:
type: string
description: |-
The name of the user setting.
Format: users/{user}/settings/{setting}, {setting} is the key for the setting.
For example, "users/123/settings/GENERAL" for general settings.
description: "The name of the user setting.\r\n Format: users/{user}/settings/{setting}, {setting} is the key for the setting.\r\n For example, \"users/123/settings/GENERAL\" for general settings."
generalSetting:
$ref: '#/components/schemas/UserSetting_GeneralSetting'
sessionsSetting:
@ -3648,10 +3464,7 @@ components:
description: The default visibility of the memo.
theme:
type: string
description: |-
The preferred theme of the user.
This references a CSS file in the web/public/themes/ directory.
If not set, the default theme will be used.
description: "The preferred theme of the user.\r\n This references a CSS file in the web/public/themes/ directory.\r\n If not set, the default theme will be used."
description: General user settings configuration.
UserSetting_SessionsSetting:
type: object
@ -3676,9 +3489,7 @@ components:
properties:
name:
type: string
description: |-
The resource name of the user whose stats these are.
Format: users/{user}
description: "The resource name of the user whose stats these are.\r\n Format: users/{user}"
memoDisplayTimestamps:
type: array
items:
@ -3726,9 +3537,7 @@ components:
properties:
name:
type: string
description: |-
The name of the webhook.
Format: users/{user}/webhooks/{webhook}
description: "The name of the webhook.\r\n Format: users/{user}/webhooks/{webhook}"
url:
type: string
description: The URL to send the webhook to.
@ -3751,9 +3560,7 @@ components:
properties:
owner:
type: string
description: |-
The name of instance owner.
Format: users/{user}
description: "The name of instance owner.\r\n Format: users/{user}"
version:
type: string
description: Version is the current version of instance.
@ -3769,9 +3576,7 @@ components:
properties:
name:
type: string
description: |-
The name of the workspace setting.
Format: workspace/settings/{setting}
description: "The name of the workspace setting.\r\n Format: workspace/settings/{setting}"
generalSetting:
$ref: '#/components/schemas/WorkspaceSetting_GeneralSetting'
storageSetting:
@ -3784,9 +3589,7 @@ components:
properties:
theme:
type: string
description: |-
theme is the name of the selected theme.
This references a CSS file in the web/public/themes/ directory.
description: "theme is the name of the selected theme.\r\n This references a CSS file in the web/public/themes/ directory."
disallowUserRegistration:
type: boolean
description: disallow_user_registration disallows user registration.
@ -3805,10 +3608,7 @@ components:
description: custom_profile is the custom profile.
weekStartDayOffset:
type: integer
description: |-
week_start_day_offset is the week start day offset from Sunday.
0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday
Default is Sunday.
description: "week_start_day_offset is the week start day offset from Sunday.\r\n 0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday\r\n Default is Sunday."
format: int32
disallowChangeUsername:
type: boolean
@ -3867,9 +3667,7 @@ components:
format: enum
filepathTemplate:
type: string
description: |-
The template of file path.
e.g. assets/{timestamp}_{filename}
description: "The template of file path.\r\n e.g. assets/{timestamp}_{filename}"
uploadSizeLimitMb:
type: string
description: The max upload size in megabytes.
@ -3877,6 +3675,22 @@ components:
allOf:
- $ref: '#/components/schemas/StorageSetting_S3Config'
description: The S3 config.
thumbnailMaxSize:
type: integer
description: The maximum size in pixels for the largest dimension of thumbnail images.
format: int32
jpegQuality:
type: integer
description: The JPEG quality (0-100) used when downscaling uploaded images.
format: int32
thumbnailJpegQuality:
type: integer
description: The JPEG quality (0-100) used when generating thumbnails.
format: int32
imageMaxSize:
type: integer
description: "The maximum size in pixels for the largest dimension when storing images.\r\n Images larger than this will be downscaled before storage.\r\n Set to 0 to disable downscaling."
format: int32
description: Storage configuration settings for workspace attachments.
tags:
- name: ActivityService

@ -509,7 +509,17 @@ 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"`
S3Config *StorageS3Config `protobuf:"bytes,4,opt,name=s3_config,json=s3Config,proto3" json:"s3_config,omitempty"`
// The maximum size in pixels for the largest dimension of thumbnail images.
ThumbnailMaxSize int32 `protobuf:"varint,5,opt,name=thumbnail_max_size,json=thumbnailMaxSize,proto3" json:"thumbnail_max_size,omitempty"`
// The JPEG quality (0-100) used when downscaling uploaded images.
JpegQuality int32 `protobuf:"varint,6,opt,name=jpeg_quality,json=jpegQuality,proto3" json:"jpeg_quality,omitempty"`
// The JPEG quality (0-100) used when generating thumbnails.
ThumbnailJpegQuality int32 `protobuf:"varint,7,opt,name=thumbnail_jpeg_quality,json=thumbnailJpegQuality,proto3" json:"thumbnail_jpeg_quality,omitempty"`
// The maximum size in pixels for the largest dimension when storing images.
// Images larger than this will be downscaled before storage.
// Set to 0 to disable downscaling.
ImageMaxSize int32 `protobuf:"varint,8,opt,name=image_max_size,json=imageMaxSize,proto3" json:"image_max_size,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -572,6 +582,34 @@ func (x *WorkspaceStorageSetting) GetS3Config() *StorageS3Config {
return nil
}
func (x *WorkspaceStorageSetting) GetThumbnailMaxSize() int32 {
if x != nil {
return x.ThumbnailMaxSize
}
return 0
}
func (x *WorkspaceStorageSetting) GetJpegQuality() int32 {
if x != nil {
return x.JpegQuality
}
return 0
}
func (x *WorkspaceStorageSetting) GetThumbnailJpegQuality() int32 {
if x != nil {
return x.ThumbnailJpegQuality
}
return 0
}
func (x *WorkspaceStorageSetting) GetImageMaxSize() int32 {
if x != nil {
return x.ImageMaxSize
}
return 0
}
// Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/
type StorageS3Config struct {
state protoimpl.MessageState `protogen:"open.v1"`
@ -804,12 +842,16 @@ 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\"\x82\x04\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" +
"\x12thumbnail_max_size\x18\x05 \x01(\x05R\x10thumbnailMaxSize\x12!\n" +
"\fjpeg_quality\x18\x06 \x01(\x05R\vjpegQuality\x124\n" +
"\x16thumbnail_jpeg_quality\x18\a \x01(\x05R\x14thumbnailJpegQuality\x12$\n" +
"\x0eimage_max_size\x18\b \x01(\x05R\fimageMaxSize\"L\n" +
"\vStorageType\x12\x1c\n" +
"\x18STORAGE_TYPE_UNSPECIFIED\x10\x00\x12\f\n" +
"\bDATABASE\x10\x01\x12\t\n" +

@ -83,6 +83,16 @@ message WorkspaceStorageSetting {
int64 upload_size_limit_mb = 3;
// The S3 config.
StorageS3Config s3_config = 4;
// The maximum size in pixels for the largest dimension of thumbnail images.
int32 thumbnail_max_size = 5;
// The JPEG quality (0-100) used when downscaling uploaded images.
int32 jpeg_quality = 6;
// The JPEG quality (0-100) used when generating thumbnails.
int32 thumbnail_jpeg_quality = 7;
// The maximum size in pixels for the largest dimension when storing images.
// Images larger than this will be downscaled before storage.
// Set to 0 to disable downscaling.
int32 image_max_size = 8;
}
// Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/

@ -99,9 +99,10 @@ func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.Creat
create.Size = int64(size)
create.Blob = request.Attachment.Content
// Downscale images before storing them if they are larger than maxAttachmentImageDimension
if util.HasPrefixes(create.Type, SupportedThumbnailMimeTypes...) {
downscaledBlob, err := downscaleImage(create.Blob, maxAttachmentImageDimension, defaultJPEGQuality)
// Downscale images before storing them if they are larger than the configured max dimension
// ImageMaxSize of 0 means no downscaling should be performed
if util.HasPrefixes(create.Type, SupportedThumbnailMimeTypes...) && workspaceStorageSetting.ImageMaxSize > 0 {
downscaledBlob, err := downscaleImage(create.Blob, int(workspaceStorageSetting.ImageMaxSize), int(workspaceStorageSetting.JpegQuality))
if err != nil {
// Log the error but continue with the original image if downscaling fails
slog.Warn("failed to downscale image attachment", slog.Any("error", err), slog.String("filename", create.Filename))
@ -539,17 +540,6 @@ func (s *APIV1Service) GetAttachmentBlob(attachment *store.Attachment) ([]byte,
return attachment.Blob, nil
}
const (
// thumbnailMaxSize is the maximum size in pixels for the largest dimension of the thumbnail image.
thumbnailMaxSize = 600
// defaultJPEGQuality is the default JPEG quality for downscaling images.
defaultJPEGQuality = 85
// defaultThumbnailJPEGQuality is the JPEG quality for generated thumbnails.
defaultThumbnailJPEGQuality = 75
// maxAttachmentImageDimension is the maximum size in pixels for the largest dimension when storing images. Images larger than this will be downscaled before storage.
maxAttachmentImageDimension = 2048
)
func downscaleImage(imageBlob []byte, maxDimension int, quality int) ([]byte, error) {
// Detect the image format before decoding
reader := bytes.NewReader(imageBlob)
@ -611,6 +601,11 @@ func downscaleImage(imageBlob []byte, maxDimension int, quality int) ([]byte, er
// getOrGenerateThumbnail returns the thumbnail image of the attachment.
func (s *APIV1Service) getOrGenerateThumbnail(attachment *store.Attachment) ([]byte, error) {
workspaceStorageSetting, err := s.Store.GetWorkspaceStorageSetting(context.Background())
if err != nil {
return nil, errors.Wrap(err, "failed to get workspace storage setting")
}
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")
@ -628,7 +623,7 @@ func (s *APIV1Service) getOrGenerateThumbnail(attachment *store.Attachment) ([]b
}
// Downscale the image
thumbnailBlob, err := downscaleImage(blob, thumbnailMaxSize, defaultThumbnailJPEGQuality)
thumbnailBlob, err := downscaleImage(blob, int(workspaceStorageSetting.ThumbnailMaxSize), int(workspaceStorageSetting.ThumbnailJpegQuality))
if err != nil {
return nil, errors.Wrap(err, "failed to downscale image")
}

@ -211,9 +211,13 @@ 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,
ThumbnailMaxSize: settingpb.ThumbnailMaxSize,
JpegQuality: settingpb.JpegQuality,
ThumbnailJpegQuality: settingpb.ThumbnailJpegQuality,
ImageMaxSize: settingpb.ImageMaxSize,
}
if settingpb.S3Config != nil {
setting.S3Config = &v1pb.WorkspaceSetting_StorageSetting_S3Config{
@ -233,9 +237,13 @@ 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,
ThumbnailMaxSize: setting.ThumbnailMaxSize,
JpegQuality: setting.JpegQuality,
ThumbnailJpegQuality: setting.ThumbnailJpegQuality,
ImageMaxSize: setting.ImageMaxSize,
}
if setting.S3Config != nil {
settingpb.S3Config = &storepb.StorageS3Config{

@ -175,9 +175,12 @@ func (s *Store) GetWorkspaceMemoRelatedSetting(ctx context.Context) (*storepb.Wo
}
const (
defaultWorkspaceStorageType = storepb.WorkspaceStorageSetting_DATABASE
defaultWorkspaceUploadSizeLimitMb = 30
defaultWorkspaceFilepathTemplate = "assets/{timestamp}_{filename}"
defaultWorkspaceStorageType = storepb.WorkspaceStorageSetting_DATABASE
defaultWorkspaceUploadSizeLimitMb = 30
defaultWorkspaceFilepathTemplate = "assets/{timestamp}_{filename}"
defaultThumbnailMaxSize = 600
defaultJPEGQuality = 85
defaultThumbnailJPEGQuality = 75
)
func (s *Store) GetWorkspaceStorageSetting(ctx context.Context) (*storepb.WorkspaceStorageSetting, error) {
@ -201,6 +204,16 @@ func (s *Store) GetWorkspaceStorageSetting(ctx context.Context) (*storepb.Worksp
if workspaceStorageSetting.FilepathTemplate == "" {
workspaceStorageSetting.FilepathTemplate = defaultWorkspaceFilepathTemplate
}
if workspaceStorageSetting.ThumbnailMaxSize == 0 {
workspaceStorageSetting.ThumbnailMaxSize = defaultThumbnailMaxSize
}
if workspaceStorageSetting.JpegQuality == 0 {
workspaceStorageSetting.JpegQuality = defaultJPEGQuality
}
if workspaceStorageSetting.ThumbnailJpegQuality == 0 {
workspaceStorageSetting.ThumbnailJpegQuality = defaultThumbnailJPEGQuality
}
// Note: ImageMaxSize of 0 is a valid value meaning "no downscaling", so we don't apply a default
s.workspaceSettingCache.Set(ctx, storepb.WorkspaceSettingKey_STORAGE.String(), &storepb.WorkspaceSetting{
Key: storepb.WorkspaceSettingKey_STORAGE,
Value: &storepb.WorkspaceSetting_StorageSetting{StorageSetting: workspaceStorageSetting},

@ -39,6 +39,19 @@ const StorageSection = observer(() => {
if (workspaceStorageSetting.uploadSizeLimitMb <= 0) {
return false;
}
// imageMaxSize can be 0 (meaning no downscaling) or positive
if (workspaceStorageSetting.imageMaxSize < 0) {
return false;
}
if (workspaceStorageSetting.jpegQuality <= 0 || workspaceStorageSetting.jpegQuality > 100) {
return false;
}
if (workspaceStorageSetting.thumbnailMaxSize <= 0) {
return false;
}
if (workspaceStorageSetting.thumbnailJpegQuality <= 0 || workspaceStorageSetting.thumbnailJpegQuality > 100) {
return false;
}
const origin = WorkspaceSetting_StorageSetting.fromPartial(
workspaceStore.getWorkspaceSettingByKey(WorkspaceSetting_Key.STORAGE)?.storageSetting || {},
@ -73,6 +86,54 @@ const StorageSection = observer(() => {
setWorkspaceStorageSetting(update);
};
const handleThumbnailMaxSizeChanged = async (event: React.FocusEvent<HTMLInputElement>) => {
let num = parseInt(event.target.value);
if (Number.isNaN(num)) {
num = 0;
}
const update: WorkspaceSetting_StorageSetting = {
...workspaceStorageSetting,
thumbnailMaxSize: num,
};
setWorkspaceStorageSetting(update);
};
const handleJpegQualityChanged = async (event: React.FocusEvent<HTMLInputElement>) => {
let num = parseInt(event.target.value);
if (Number.isNaN(num)) {
num = 0;
}
const update: WorkspaceSetting_StorageSetting = {
...workspaceStorageSetting,
jpegQuality: num,
};
setWorkspaceStorageSetting(update);
};
const handleThumbnailJpegQualityChanged = async (event: React.FocusEvent<HTMLInputElement>) => {
let num = parseInt(event.target.value);
if (Number.isNaN(num)) {
num = 0;
}
const update: WorkspaceSetting_StorageSetting = {
...workspaceStorageSetting,
thumbnailJpegQuality: num,
};
setWorkspaceStorageSetting(update);
};
const handleImageMaxSizeChanged = async (event: React.FocusEvent<HTMLInputElement>) => {
let num = parseInt(event.target.value);
if (Number.isNaN(num)) {
num = 0;
}
const update: WorkspaceSetting_StorageSetting = {
...workspaceStorageSetting,
imageMaxSize: num,
};
setWorkspaceStorageSetting(update);
};
const handleFilepathTemplateChanged = async (event: React.FocusEvent<HTMLInputElement>) => {
const update: WorkspaceSetting_StorageSetting = {
...workspaceStorageSetting,
@ -171,7 +232,7 @@ const StorageSection = observer(() => {
</Tooltip>
</TooltipProvider>
</div>
<Input className="w-16 font-mono" value={workspaceStorageSetting.uploadSizeLimitMb} onChange={handleMaxUploadSizeChanged} />
<Input className="w-20 font-mono" value={workspaceStorageSetting.uploadSizeLimitMb} onChange={handleMaxUploadSizeChanged} />
</div>
{workspaceStorageSetting.storageType !== WorkspaceSetting_StorageSetting_StorageType.DATABASE && (
<div className="w-full flex flex-row justify-between items-center">
@ -240,6 +301,71 @@ const StorageSection = observer(() => {
</div>
</>
)}
<div className="w-full flex flex-row justify-between items-center">
<div className="flex flex-row items-center">
<span className="text-muted-foreground mr-1">Maximum image size (px)</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<HelpCircleIcon className="w-4 h-auto" />
</TooltipTrigger>
<TooltipContent>
<p>Maximum size in pixels for the largest dimension when storing images. Images larger than this will be downscaled. Set to 0 to disable downscaling (default: 0, no downscaling).</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Input className="w-20 font-mono" value={workspaceStorageSetting.imageMaxSize} onChange={handleImageMaxSizeChanged} />
</div>
<div className="w-full flex flex-row justify-between items-center">
<div className="flex flex-row items-center">
<span className="text-muted-foreground mr-1">JPEG quality</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<HelpCircleIcon className="w-4 h-auto" />
</TooltipTrigger>
<TooltipContent>
<p>JPEG quality (0-100) used when downscaling uploaded images. Higher values = better quality but larger file size (default: 85).</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Input className="w-20 font-mono" value={workspaceStorageSetting.jpegQuality} onChange={handleJpegQualityChanged} />
</div>
<div className="w-full flex flex-row justify-between items-center">
<div className="flex flex-row items-center">
<span className="text-muted-foreground mr-1">Thumbnail size (px)</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<HelpCircleIcon className="w-4 h-auto" />
</TooltipTrigger>
<TooltipContent>
<p>Maximum size in pixels for the largest dimension of thumbnail images (default: 600).</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Input className="w-20 font-mono" value={workspaceStorageSetting.thumbnailMaxSize} onChange={handleThumbnailMaxSizeChanged} />
</div>
<div className="w-full flex flex-row justify-between items-center">
<div className="flex flex-row items-center">
<span className="text-muted-foreground mr-1">Thumbnail JPEG quality</span>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<HelpCircleIcon className="w-4 h-auto" />
</TooltipTrigger>
<TooltipContent>
<p>JPEG quality (0-100) used when generating thumbnails. Lower values save space (default: 75).</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Input className="w-20 font-mono" value={workspaceStorageSetting.thumbnailJpegQuality} onChange={handleThumbnailJpegQualityChanged} />
</div>
<div>
<Button disabled={!allowSaveStorageSetting} onClick={saveWorkspaceStorageSetting}>
{t("common.save")}

@ -141,7 +141,21 @@ 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;
/** The maximum size in pixels for the largest dimension of thumbnail images. */
thumbnailMaxSize: number;
/** The JPEG quality (0-100) used when downscaling uploaded images. */
jpegQuality: number;
/** The JPEG quality (0-100) used when generating thumbnails. */
thumbnailJpegQuality: number;
/**
* The maximum size in pixels for the largest dimension when storing images.
* Images larger than this will be downscaled before storage.
* Set to 0 to disable downscaling.
*/
imageMaxSize: number;
}
/** Storage type enumeration for different storage backends. */
@ -705,6 +719,10 @@ function createBaseWorkspaceSetting_StorageSetting(): WorkspaceSetting_StorageSe
filepathTemplate: "",
uploadSizeLimitMb: 0,
s3Config: undefined,
thumbnailMaxSize: 0,
jpegQuality: 0,
thumbnailJpegQuality: 0,
imageMaxSize: 0,
};
}
@ -722,6 +740,18 @@ export const WorkspaceSetting_StorageSetting: MessageFns<WorkspaceSetting_Storag
if (message.s3Config !== undefined) {
WorkspaceSetting_StorageSetting_S3Config.encode(message.s3Config, writer.uint32(34).fork()).join();
}
if (message.thumbnailMaxSize !== 0) {
writer.uint32(40).int32(message.thumbnailMaxSize);
}
if (message.jpegQuality !== 0) {
writer.uint32(48).int32(message.jpegQuality);
}
if (message.thumbnailJpegQuality !== 0) {
writer.uint32(56).int32(message.thumbnailJpegQuality);
}
if (message.imageMaxSize !== 0) {
writer.uint32(64).int32(message.imageMaxSize);
}
return writer;
},
@ -764,6 +794,38 @@ export const WorkspaceSetting_StorageSetting: MessageFns<WorkspaceSetting_Storag
message.s3Config = WorkspaceSetting_StorageSetting_S3Config.decode(reader, reader.uint32());
continue;
}
case 5: {
if (tag !== 40) {
break;
}
message.thumbnailMaxSize = reader.int32();
continue;
}
case 6: {
if (tag !== 48) {
break;
}
message.jpegQuality = reader.int32();
continue;
}
case 7: {
if (tag !== 56) {
break;
}
message.thumbnailJpegQuality = reader.int32();
continue;
}
case 8: {
if (tag !== 64) {
break;
}
message.imageMaxSize = reader.int32();
continue;
}
}
if ((tag & 7) === 4 || tag === 0) {
break;
@ -784,6 +846,10 @@ export const WorkspaceSetting_StorageSetting: MessageFns<WorkspaceSetting_Storag
message.s3Config = (object.s3Config !== undefined && object.s3Config !== null)
? WorkspaceSetting_StorageSetting_S3Config.fromPartial(object.s3Config)
: undefined;
message.thumbnailMaxSize = object.thumbnailMaxSize ?? 0;
message.jpegQuality = object.jpegQuality ?? 0;
message.thumbnailJpegQuality = object.thumbnailJpegQuality ?? 0;
message.imageMaxSize = object.imageMaxSize ?? 0;
return message;
},
};

Loading…
Cancel
Save