Introduce settings for thumbnail and image downsizing

pull/5184/head
Florian Dewald 2 weeks ago
parent d5cde36a58
commit 0ede2e8410

@ -147,6 +147,16 @@ message WorkspaceSetting {
} }
// The S3 config. // The S3 config.
S3Config s3_config = 4; 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. // Memo-related workspace settings and policies.

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

@ -589,7 +589,17 @@ type WorkspaceSetting_StorageSetting struct {
// The max upload size in megabytes. // 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"` UploadSizeLimitMb int64 `protobuf:"varint,3,opt,name=upload_size_limit_mb,json=uploadSizeLimitMb,proto3" json:"upload_size_limit_mb,omitempty"`
// The S3 config. // 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 unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -652,6 +662,34 @@ func (x *WorkspaceSetting_StorageSetting) GetS3Config() *WorkspaceSetting_Storag
return nil 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. // Memo-related workspace settings and policies.
type WorkspaceSetting_MemoRelatedSetting struct { type WorkspaceSetting_MemoRelatedSetting struct {
state protoimpl.MessageState `protogen:"open.v1"` 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" + "\aversion\x18\x02 \x01(\tR\aversion\x12\x12\n" +
"\x04mode\x18\x03 \x01(\tR\x04mode\x12!\n" + "\x04mode\x18\x03 \x01(\tR\x04mode\x12!\n" +
"\finstance_url\x18\x06 \x01(\tR\vinstanceUrl\"\x1c\n" + "\finstance_url\x18\x06 \x01(\tR\vinstanceUrl\"\x1c\n" +
"\x1aGetWorkspaceProfileRequest\"\x97\x11\n" + "\x1aGetWorkspaceProfileRequest\"\xc4\x12\n" +
"\x10WorkspaceSetting\x12\x17\n" + "\x10WorkspaceSetting\x12\x17\n" +
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12X\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" + "\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" + "\x05title\x18\x01 \x01(\tR\x05title\x12 \n" +
"\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" +
"\blogo_url\x18\x03 \x01(\tR\alogoUrl\x12\x16\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" + "\x0eStorageSetting\x12\\\n" +
"\fstorage_type\x18\x01 \x01(\x0e29.memos.api.v1.WorkspaceSetting.StorageSetting.StorageTypeR\vstorageType\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" + "\x11filepath_template\x18\x02 \x01(\tR\x10filepathTemplate\x12/\n" +
"\x14upload_size_limit_mb\x18\x03 \x01(\x03R\x11uploadSizeLimitMb\x12S\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" + "\bS3Config\x12\"\n" +
"\raccess_key_id\x18\x01 \x01(\tR\vaccessKeyId\x12*\n" + "\raccess_key_id\x18\x01 \x01(\tR\vaccessKeyId\x12*\n" +
"\x11access_key_secret\x18\x02 \x01(\tR\x0faccessKeySecret\x12\x1a\n" + "\x11access_key_secret\x18\x02 \x01(\tR\x0faccessKeySecret\x12\x1a\n" +

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

@ -509,7 +509,17 @@ type WorkspaceStorageSetting struct {
// The max upload size in megabytes. // 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"` UploadSizeLimitMb int64 `protobuf:"varint,3,opt,name=upload_size_limit_mb,json=uploadSizeLimitMb,proto3" json:"upload_size_limit_mb,omitempty"`
// The S3 config. // 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 unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -572,6 +582,34 @@ func (x *WorkspaceStorageSetting) GetS3Config() *StorageS3Config {
return nil 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/ // Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/
type StorageS3Config struct { type StorageS3Config struct {
state protoimpl.MessageState `protogen:"open.v1"` 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" + "\x05title\x18\x01 \x01(\tR\x05title\x12 \n" +
"\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" +
"\blogo_url\x18\x03 \x01(\tR\alogoUrl\x12\x16\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" + "\x17WorkspaceStorageSetting\x12S\n" +
"\fstorage_type\x18\x01 \x01(\x0e20.memos.store.WorkspaceStorageSetting.StorageTypeR\vstorageType\x12+\n" + "\fstorage_type\x18\x01 \x01(\x0e20.memos.store.WorkspaceStorageSetting.StorageTypeR\vstorageType\x12+\n" +
"\x11filepath_template\x18\x02 \x01(\tR\x10filepathTemplate\x12/\n" + "\x11filepath_template\x18\x02 \x01(\tR\x10filepathTemplate\x12/\n" +
"\x14upload_size_limit_mb\x18\x03 \x01(\x03R\x11uploadSizeLimitMb\x129\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" + "\vStorageType\x12\x1c\n" +
"\x18STORAGE_TYPE_UNSPECIFIED\x10\x00\x12\f\n" + "\x18STORAGE_TYPE_UNSPECIFIED\x10\x00\x12\f\n" +
"\bDATABASE\x10\x01\x12\t\n" + "\bDATABASE\x10\x01\x12\t\n" +

@ -83,6 +83,16 @@ message WorkspaceStorageSetting {
int64 upload_size_limit_mb = 3; int64 upload_size_limit_mb = 3;
// The S3 config. // The S3 config.
StorageS3Config s3_config = 4; 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/ // 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.Size = int64(size)
create.Blob = request.Attachment.Content create.Blob = request.Attachment.Content
// Downscale images before storing them if they are larger than maxAttachmentImageDimension // Downscale images before storing them if they are larger than the configured max dimension
if util.HasPrefixes(create.Type, SupportedThumbnailMimeTypes...) { // ImageMaxSize of 0 means no downscaling should be performed
downscaledBlob, err := downscaleImage(create.Blob, maxAttachmentImageDimension, defaultJPEGQuality) if util.HasPrefixes(create.Type, SupportedThumbnailMimeTypes...) && workspaceStorageSetting.ImageMaxSize > 0 {
downscaledBlob, err := downscaleImage(create.Blob, int(workspaceStorageSetting.ImageMaxSize), int(workspaceStorageSetting.JpegQuality))
if err != nil { if err != nil {
// Log the error but continue with the original image if downscaling fails // 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)) 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 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) { func downscaleImage(imageBlob []byte, maxDimension int, quality int) ([]byte, error) {
// Detect the image format before decoding // Detect the image format before decoding
reader := bytes.NewReader(imageBlob) 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. // getOrGenerateThumbnail returns the thumbnail image of the attachment.
func (s *APIV1Service) getOrGenerateThumbnail(attachment *store.Attachment) ([]byte, error) { 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) thumbnailCacheFolder := filepath.Join(s.Profile.Data, ThumbnailCacheFolder)
if err := os.MkdirAll(thumbnailCacheFolder, os.ModePerm); err != nil { if err := os.MkdirAll(thumbnailCacheFolder, os.ModePerm); err != nil {
return nil, errors.Wrap(err, "failed to create thumbnail cache folder") 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 // Downscale the image
thumbnailBlob, err := downscaleImage(blob, thumbnailMaxSize, defaultThumbnailJPEGQuality) thumbnailBlob, err := downscaleImage(blob, int(workspaceStorageSetting.ThumbnailMaxSize), int(workspaceStorageSetting.ThumbnailJpegQuality))
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to downscale image") return nil, errors.Wrap(err, "failed to downscale image")
} }

@ -211,9 +211,13 @@ func convertWorkspaceStorageSettingFromStore(settingpb *storepb.WorkspaceStorage
return nil return nil
} }
setting := &v1pb.WorkspaceSetting_StorageSetting{ setting := &v1pb.WorkspaceSetting_StorageSetting{
StorageType: v1pb.WorkspaceSetting_StorageSetting_StorageType(settingpb.StorageType), StorageType: v1pb.WorkspaceSetting_StorageSetting_StorageType(settingpb.StorageType),
FilepathTemplate: settingpb.FilepathTemplate, FilepathTemplate: settingpb.FilepathTemplate,
UploadSizeLimitMb: settingpb.UploadSizeLimitMb, UploadSizeLimitMb: settingpb.UploadSizeLimitMb,
ThumbnailMaxSize: settingpb.ThumbnailMaxSize,
JpegQuality: settingpb.JpegQuality,
ThumbnailJpegQuality: settingpb.ThumbnailJpegQuality,
ImageMaxSize: settingpb.ImageMaxSize,
} }
if settingpb.S3Config != nil { if settingpb.S3Config != nil {
setting.S3Config = &v1pb.WorkspaceSetting_StorageSetting_S3Config{ setting.S3Config = &v1pb.WorkspaceSetting_StorageSetting_S3Config{
@ -233,9 +237,13 @@ func convertWorkspaceStorageSettingToStore(setting *v1pb.WorkspaceSetting_Storag
return nil return nil
} }
settingpb := &storepb.WorkspaceStorageSetting{ settingpb := &storepb.WorkspaceStorageSetting{
StorageType: storepb.WorkspaceStorageSetting_StorageType(setting.StorageType), StorageType: storepb.WorkspaceStorageSetting_StorageType(setting.StorageType),
FilepathTemplate: setting.FilepathTemplate, FilepathTemplate: setting.FilepathTemplate,
UploadSizeLimitMb: setting.UploadSizeLimitMb, UploadSizeLimitMb: setting.UploadSizeLimitMb,
ThumbnailMaxSize: setting.ThumbnailMaxSize,
JpegQuality: setting.JpegQuality,
ThumbnailJpegQuality: setting.ThumbnailJpegQuality,
ImageMaxSize: setting.ImageMaxSize,
} }
if setting.S3Config != nil { if setting.S3Config != nil {
settingpb.S3Config = &storepb.StorageS3Config{ settingpb.S3Config = &storepb.StorageS3Config{

@ -175,9 +175,12 @@ func (s *Store) GetWorkspaceMemoRelatedSetting(ctx context.Context) (*storepb.Wo
} }
const ( const (
defaultWorkspaceStorageType = storepb.WorkspaceStorageSetting_DATABASE defaultWorkspaceStorageType = storepb.WorkspaceStorageSetting_DATABASE
defaultWorkspaceUploadSizeLimitMb = 30 defaultWorkspaceUploadSizeLimitMb = 30
defaultWorkspaceFilepathTemplate = "assets/{timestamp}_{filename}" defaultWorkspaceFilepathTemplate = "assets/{timestamp}_{filename}"
defaultThumbnailMaxSize = 600
defaultJPEGQuality = 85
defaultThumbnailJPEGQuality = 75
) )
func (s *Store) GetWorkspaceStorageSetting(ctx context.Context) (*storepb.WorkspaceStorageSetting, error) { 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 == "" { if workspaceStorageSetting.FilepathTemplate == "" {
workspaceStorageSetting.FilepathTemplate = defaultWorkspaceFilepathTemplate 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{ s.workspaceSettingCache.Set(ctx, storepb.WorkspaceSettingKey_STORAGE.String(), &storepb.WorkspaceSetting{
Key: storepb.WorkspaceSettingKey_STORAGE, Key: storepb.WorkspaceSettingKey_STORAGE,
Value: &storepb.WorkspaceSetting_StorageSetting{StorageSetting: workspaceStorageSetting}, Value: &storepb.WorkspaceSetting_StorageSetting{StorageSetting: workspaceStorageSetting},

@ -39,6 +39,19 @@ const StorageSection = observer(() => {
if (workspaceStorageSetting.uploadSizeLimitMb <= 0) { if (workspaceStorageSetting.uploadSizeLimitMb <= 0) {
return false; 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( const origin = WorkspaceSetting_StorageSetting.fromPartial(
workspaceStore.getWorkspaceSettingByKey(WorkspaceSetting_Key.STORAGE)?.storageSetting || {}, workspaceStore.getWorkspaceSettingByKey(WorkspaceSetting_Key.STORAGE)?.storageSetting || {},
@ -73,6 +86,54 @@ const StorageSection = observer(() => {
setWorkspaceStorageSetting(update); 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 handleFilepathTemplateChanged = async (event: React.FocusEvent<HTMLInputElement>) => {
const update: WorkspaceSetting_StorageSetting = { const update: WorkspaceSetting_StorageSetting = {
...workspaceStorageSetting, ...workspaceStorageSetting,
@ -171,7 +232,7 @@ const StorageSection = observer(() => {
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
</div> </div>
<Input className="w-16 font-mono" value={workspaceStorageSetting.uploadSizeLimitMb} onChange={handleMaxUploadSizeChanged} /> <Input className="w-20 font-mono" value={workspaceStorageSetting.uploadSizeLimitMb} onChange={handleMaxUploadSizeChanged} />
</div> </div>
{workspaceStorageSetting.storageType !== WorkspaceSetting_StorageSetting_StorageType.DATABASE && ( {workspaceStorageSetting.storageType !== WorkspaceSetting_StorageSetting_StorageType.DATABASE && (
<div className="w-full flex flex-row justify-between items-center"> <div className="w-full flex flex-row justify-between items-center">
@ -240,6 +301,71 @@ const StorageSection = observer(() => {
</div> </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> <div>
<Button disabled={!allowSaveStorageSetting} onClick={saveWorkspaceStorageSetting}> <Button disabled={!allowSaveStorageSetting} onClick={saveWorkspaceStorageSetting}>
{t("common.save")} {t("common.save")}

@ -141,7 +141,21 @@ export interface WorkspaceSetting_StorageSetting {
/** The max upload size in megabytes. */ /** The max upload size in megabytes. */
uploadSizeLimitMb: number; uploadSizeLimitMb: number;
/** The S3 config. */ /** 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. */ /** Storage type enumeration for different storage backends. */
@ -705,6 +719,10 @@ function createBaseWorkspaceSetting_StorageSetting(): WorkspaceSetting_StorageSe
filepathTemplate: "", filepathTemplate: "",
uploadSizeLimitMb: 0, uploadSizeLimitMb: 0,
s3Config: undefined, 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) { if (message.s3Config !== undefined) {
WorkspaceSetting_StorageSetting_S3Config.encode(message.s3Config, writer.uint32(34).fork()).join(); 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; return writer;
}, },
@ -764,6 +794,38 @@ export const WorkspaceSetting_StorageSetting: MessageFns<WorkspaceSetting_Storag
message.s3Config = WorkspaceSetting_StorageSetting_S3Config.decode(reader, reader.uint32()); message.s3Config = WorkspaceSetting_StorageSetting_S3Config.decode(reader, reader.uint32());
continue; 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) { if ((tag & 7) === 4 || tag === 0) {
break; break;
@ -784,6 +846,10 @@ export const WorkspaceSetting_StorageSetting: MessageFns<WorkspaceSetting_Storag
message.s3Config = (object.s3Config !== undefined && object.s3Config !== null) message.s3Config = (object.s3Config !== undefined && object.s3Config !== null)
? WorkspaceSetting_StorageSetting_S3Config.fromPartial(object.s3Config) ? WorkspaceSetting_StorageSetting_S3Config.fromPartial(object.s3Config)
: undefined; : undefined;
message.thumbnailMaxSize = object.thumbnailMaxSize ?? 0;
message.jpegQuality = object.jpegQuality ?? 0;
message.thumbnailJpegQuality = object.thumbnailJpegQuality ?? 0;
message.imageMaxSize = object.imageMaxSize ?? 0;
return message; return message;
}, },
}; };

Loading…
Cancel
Save