Commit 3e6e56b0 authored by Steven's avatar Steven

refactor: update workspace store definition

parent 6d842711
...@@ -4,6 +4,7 @@ package memos.api.v2; ...@@ -4,6 +4,7 @@ package memos.api.v2;
import "google/api/annotations.proto"; import "google/api/annotations.proto";
import "google/api/client.proto"; import "google/api/client.proto";
import "google/api/field_behavior.proto";
import "google/protobuf/field_mask.proto"; import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto"; import "google/protobuf/timestamp.proto";
...@@ -51,29 +52,25 @@ message Resource { ...@@ -51,29 +52,25 @@ message Resource {
// The user defined id of the resource. // The user defined id of the resource.
string uid = 2; string uid = 2;
google.protobuf.Timestamp create_time = 3; google.protobuf.Timestamp create_time = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
string filename = 4; string filename = 4;
string external_link = 5; bytes content = 5 [(google.api.field_behavior) = INPUT_ONLY];
string type = 6; string external_link = 6;
int64 size = 7; string type = 7;
int64 size = 8;
// The related memo.
// Format: memos/{id} // Format: memos/{id}
optional string memo = 8; optional string memo = 9;
} }
message CreateResourceRequest { message CreateResourceRequest {
string filename = 1; Resource resource = 1;
string external_link = 2;
string type = 3;
// Format: memos/{id}
optional string memo = 4;
} }
message CreateResourceResponse { message CreateResourceResponse {
...@@ -95,6 +92,9 @@ message SearchResourcesResponse { ...@@ -95,6 +92,9 @@ message SearchResourcesResponse {
} }
message GetResourceRequest { message GetResourceRequest {
// The name of the resource.
// Format: resources/{id}
// id is the system generated unique identifier.
string name = 1; string name = 1;
} }
...@@ -113,6 +113,9 @@ message UpdateResourceResponse { ...@@ -113,6 +113,9 @@ message UpdateResourceResponse {
} }
message DeleteResourceRequest { message DeleteResourceRequest {
// The name of the resource.
// Format: resources/{id}
// id is the system generated unique identifier.
string name = 1; string name = 1;
} }
......
...@@ -1609,10 +1609,7 @@ Used internally for obfuscating the page token. ...@@ -1609,10 +1609,7 @@ Used internally for obfuscating the page token.
| Field | Type | Label | Description | | Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- | | ----- | ---- | ----- | ----------- |
| filename | [string](#string) | | | | resource | [Resource](#memos-api-v2-Resource) | | |
| external_link | [string](#string) | | |
| type | [string](#string) | | |
| memo | [string](#string) | optional | Format: memos/{id} |
...@@ -1642,7 +1639,7 @@ Used internally for obfuscating the page token. ...@@ -1642,7 +1639,7 @@ Used internally for obfuscating the page token.
| Field | Type | Label | Description | | Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- | | ----- | ---- | ----- | ----------- |
| name | [string](#string) | | | | name | [string](#string) | | The name of the resource. Format: resources/{id} id is the system generated unique identifier. |
...@@ -1667,7 +1664,7 @@ Used internally for obfuscating the page token. ...@@ -1667,7 +1664,7 @@ Used internally for obfuscating the page token.
| Field | Type | Label | Description | | Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- | | ----- | ---- | ----- | ----------- |
| name | [string](#string) | | | | name | [string](#string) | | The name of the resource. Format: resources/{id} id is the system generated unique identifier. |
...@@ -1726,10 +1723,11 @@ Used internally for obfuscating the page token. ...@@ -1726,10 +1723,11 @@ Used internally for obfuscating the page token.
| uid | [string](#string) | | The user defined id of the resource. | | uid | [string](#string) | | The user defined id of the resource. |
| create_time | [google.protobuf.Timestamp](#google-protobuf-Timestamp) | | | | create_time | [google.protobuf.Timestamp](#google-protobuf-Timestamp) | | |
| filename | [string](#string) | | | | filename | [string](#string) | | |
| content | [bytes](#bytes) | | |
| external_link | [string](#string) | | | | external_link | [string](#string) | | |
| type | [string](#string) | | | | type | [string](#string) | | |
| size | [int64](#int64) | | | | size | [int64](#int64) | | |
| memo | [string](#string) | optional | Format: memos/{id} | | memo | [string](#string) | optional | The related memo. Format: memos/{id} |
......
This diff is collapsed.
...@@ -26,6 +26,13 @@ ...@@ -26,6 +26,13 @@
- [Reaction.Type](#memos-store-Reaction-Type) - [Reaction.Type](#memos-store-Reaction-Type)
- [store/storage.proto](#store_storage-proto)
- [S3Config](#memos-store-S3Config)
- [Storage](#memos-store-Storage)
- [StorageConfig](#memos-store-StorageConfig)
- [Storage.Type](#memos-store-Storage-Type)
- [store/user_setting.proto](#store_user_setting-proto) - [store/user_setting.proto](#store_user_setting-proto)
- [AccessTokensUserSetting](#memos-store-AccessTokensUserSetting) - [AccessTokensUserSetting](#memos-store-AccessTokensUserSetting)
- [AccessTokensUserSetting.AccessToken](#memos-store-AccessTokensUserSetting-AccessToken) - [AccessTokensUserSetting.AccessToken](#memos-store-AccessTokensUserSetting-AccessToken)
...@@ -37,6 +44,8 @@ ...@@ -37,6 +44,8 @@
- [Webhook](#memos-store-Webhook) - [Webhook](#memos-store-Webhook)
- [store/workspace_setting.proto](#store_workspace_setting-proto) - [store/workspace_setting.proto](#store_workspace_setting-proto)
- [WorkspaceBasicSetting](#memos-store-WorkspaceBasicSetting)
- [WorkspaceCustomProfile](#memos-store-WorkspaceCustomProfile)
- [WorkspaceGeneralSetting](#memos-store-WorkspaceGeneralSetting) - [WorkspaceGeneralSetting](#memos-store-WorkspaceGeneralSetting)
- [WorkspaceMemoRelatedSetting](#memos-store-WorkspaceMemoRelatedSetting) - [WorkspaceMemoRelatedSetting](#memos-store-WorkspaceMemoRelatedSetting)
- [WorkspaceSetting](#memos-store-WorkspaceSetting) - [WorkspaceSetting](#memos-store-WorkspaceSetting)
...@@ -314,6 +323,90 @@ ...@@ -314,6 +323,90 @@
<a name="store_storage-proto"></a>
<p align="right"><a href="#top">Top</a></p>
## store/storage.proto
<a name="memos-store-S3Config"></a>
### S3Config
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| end_point | [string](#string) | | |
| path | [string](#string) | | |
| region | [string](#string) | | |
| access_key | [string](#string) | | |
| secret_key | [string](#string) | | |
| bucket | [string](#string) | | |
| url_prefix | [string](#string) | | |
| url_suffix | [string](#string) | | |
| pre_sign | [bool](#bool) | | |
<a name="memos-store-Storage"></a>
### Storage
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| id | [int32](#int32) | | |
| name | [string](#string) | | |
| type | [Storage.Type](#memos-store-Storage-Type) | | |
| config | [StorageConfig](#memos-store-StorageConfig) | | |
<a name="memos-store-StorageConfig"></a>
### StorageConfig
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| s3_config | [S3Config](#memos-store-S3Config) | | |
<a name="memos-store-Storage-Type"></a>
### Storage.Type
| Name | Number | Description |
| ---- | ------ | ----------- |
| TYPE_UNSPECIFIED | 0 | |
| S3 | 1 | |
<a name="store_user_setting-proto"></a> <a name="store_user_setting-proto"></a>
<p align="right"><a href="#top">Top</a></p> <p align="right"><a href="#top">Top</a></p>
...@@ -442,6 +535,41 @@ ...@@ -442,6 +535,41 @@
<a name="memos-store-WorkspaceBasicSetting"></a>
### WorkspaceBasicSetting
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| server_id | [string](#string) | | |
| secret_key | [string](#string) | | |
<a name="memos-store-WorkspaceCustomProfile"></a>
### WorkspaceCustomProfile
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| title | [string](#string) | | |
| description | [string](#string) | | |
| logo_url | [string](#string) | | |
| locale | [string](#string) | | |
| appearance | [string](#string) | | |
<a name="memos-store-WorkspaceGeneralSetting"></a> <a name="memos-store-WorkspaceGeneralSetting"></a>
### WorkspaceGeneralSetting ### WorkspaceGeneralSetting
...@@ -455,6 +583,7 @@ ...@@ -455,6 +583,7 @@
| disallow_password_login | [bool](#bool) | | disallow_password_login is the flag to disallow password login. | | disallow_password_login | [bool](#bool) | | disallow_password_login is the flag to disallow password login. |
| additional_script | [string](#string) | | additional_script is the additional script. | | additional_script | [string](#string) | | additional_script is the additional script. |
| additional_style | [string](#string) | | additional_style is the additional style. | | additional_style | [string](#string) | | additional_style is the additional style. |
| custom_profile | [WorkspaceCustomProfile](#memos-store-WorkspaceCustomProfile) | | custom_profile is the custom profile. |
...@@ -486,6 +615,7 @@ ...@@ -486,6 +615,7 @@
| Field | Type | Label | Description | | Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- | | ----- | ---- | ----- | ----------- |
| key | [WorkspaceSettingKey](#memos-store-WorkspaceSettingKey) | | | | key | [WorkspaceSettingKey](#memos-store-WorkspaceSettingKey) | | |
| basic_setting | [WorkspaceBasicSetting](#memos-store-WorkspaceBasicSetting) | | |
| general_setting | [WorkspaceGeneralSetting](#memos-store-WorkspaceGeneralSetting) | | | | general_setting | [WorkspaceGeneralSetting](#memos-store-WorkspaceGeneralSetting) | | |
| storage_setting | [WorkspaceStorageSetting](#memos-store-WorkspaceStorageSetting) | | | | storage_setting | [WorkspaceStorageSetting](#memos-store-WorkspaceStorageSetting) | | |
| memo_related_setting | [WorkspaceMemoRelatedSetting](#memos-store-WorkspaceMemoRelatedSetting) | | | | memo_related_setting | [WorkspaceMemoRelatedSetting](#memos-store-WorkspaceMemoRelatedSetting) | | |
...@@ -505,6 +635,7 @@ ...@@ -505,6 +635,7 @@
| Field | Type | Label | Description | | Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- | | ----- | ---- | ----- | ----------- |
| storage_type | [WorkspaceStorageSetting.StorageType](#memos-store-WorkspaceStorageSetting-StorageType) | | storage_type is the storage type. | | storage_type | [WorkspaceStorageSetting.StorageType](#memos-store-WorkspaceStorageSetting-StorageType) | | storage_type is the storage type. |
| actived_external_storage_id | [int32](#int32) | optional | The id of actived external storage. |
| local_storage_path | [string](#string) | | The local storage path for STORAGE_TYPE_LOCAL. e.g. assets/{timestamp}_{filename} | | local_storage_path | [string](#string) | | The local storage path for STORAGE_TYPE_LOCAL. e.g. assets/{timestamp}_{filename} |
| upload_size_limit_mb | [int64](#int64) | | The max upload size in megabytes. | | upload_size_limit_mb | [int64](#int64) | | The max upload size in megabytes. |
...@@ -538,10 +669,11 @@ ...@@ -538,10 +669,11 @@
| Name | Number | Description | | Name | Number | Description |
| ---- | ------ | ----------- | | ---- | ------ | ----------- |
| WORKSPACE_SETTING_KEY_UNSPECIFIED | 0 | | | WORKSPACE_SETTING_KEY_UNSPECIFIED | 0 | |
| WORKSPACE_SETTING_GENERAL | 1 | WORKSPACE_SETTING_GENERAL is the key for general settings. | | WORKSPACE_SETTING_BASIC | 1 | WORKSPACE_SETTING_BASIC is the key for basic settings. |
| WORKSPACE_SETTING_STORAGE | 2 | WORKSPACE_SETTING_STORAGE is the key for storage settings. | | WORKSPACE_SETTING_GENERAL | 2 | WORKSPACE_SETTING_GENERAL is the key for general settings. |
| WORKSPACE_SETTING_MEMO_RELATED | 3 | WORKSPACE_SETTING_MEMO_RELATED is the key for memo related settings. | | WORKSPACE_SETTING_STORAGE | 3 | WORKSPACE_SETTING_STORAGE is the key for storage settings. |
| WORKSPACE_SETTING_TELEGRAM_INTEGRATION | 4 | WORKSPACE_SETTING_TELEGRAM_INTEGRATION is the key for telegram integration settings. | | WORKSPACE_SETTING_MEMO_RELATED | 4 | WORKSPACE_SETTING_MEMO_RELATED is the key for memo related settings. |
| WORKSPACE_SETTING_TELEGRAM_INTEGRATION | 5 | WORKSPACE_SETTING_TELEGRAM_INTEGRATION is the key for telegram integration settings. |
......
This diff is collapsed.
This diff is collapsed.
syntax = "proto3";
package memos.store;
option go_package = "gen/store";
message Storage {
int32 id = 1;
string name = 2;
enum Type {
TYPE_UNSPECIFIED = 0;
S3 = 1;
}
Type type = 3;
StorageConfig config = 4;
}
message StorageConfig {
oneof storage_config {
S3Config s3_config = 1;
}
}
message S3Config {
string end_point = 1;
string path = 2;
string region = 3;
string access_key = 4;
string secret_key = 5;
string bucket = 6;
string url_prefix = 7;
string url_suffix = 8;
bool pre_sign = 9;
}
...@@ -6,26 +6,34 @@ option go_package = "gen/store"; ...@@ -6,26 +6,34 @@ option go_package = "gen/store";
enum WorkspaceSettingKey { enum WorkspaceSettingKey {
WORKSPACE_SETTING_KEY_UNSPECIFIED = 0; WORKSPACE_SETTING_KEY_UNSPECIFIED = 0;
// WORKSPACE_SETTING_BASIC is the key for basic settings.
WORKSPACE_SETTING_BASIC = 1;
// WORKSPACE_SETTING_GENERAL is the key for general settings. // WORKSPACE_SETTING_GENERAL is the key for general settings.
WORKSPACE_SETTING_GENERAL = 1; WORKSPACE_SETTING_GENERAL = 2;
// WORKSPACE_SETTING_STORAGE is the key for storage settings. // WORKSPACE_SETTING_STORAGE is the key for storage settings.
WORKSPACE_SETTING_STORAGE = 2; WORKSPACE_SETTING_STORAGE = 3;
// WORKSPACE_SETTING_MEMO_RELATED is the key for memo related settings. // WORKSPACE_SETTING_MEMO_RELATED is the key for memo related settings.
WORKSPACE_SETTING_MEMO_RELATED = 3; WORKSPACE_SETTING_MEMO_RELATED = 4;
// WORKSPACE_SETTING_TELEGRAM_INTEGRATION is the key for telegram integration settings. // WORKSPACE_SETTING_TELEGRAM_INTEGRATION is the key for telegram integration settings.
WORKSPACE_SETTING_TELEGRAM_INTEGRATION = 4; WORKSPACE_SETTING_TELEGRAM_INTEGRATION = 5;
} }
message WorkspaceSetting { message WorkspaceSetting {
WorkspaceSettingKey key = 1; WorkspaceSettingKey key = 1;
oneof value { oneof value {
WorkspaceGeneralSetting general_setting = 2; WorkspaceBasicSetting basic_setting = 2;
WorkspaceStorageSetting storage_setting = 3; WorkspaceGeneralSetting general_setting = 3;
WorkspaceMemoRelatedSetting memo_related_setting = 4; WorkspaceStorageSetting storage_setting = 4;
WorkspaceTelegramIntegrationSetting telegram_integration_setting = 5; WorkspaceMemoRelatedSetting memo_related_setting = 5;
WorkspaceTelegramIntegrationSetting telegram_integration_setting = 6;
} }
} }
message WorkspaceBasicSetting {
string server_id = 1;
string secret_key = 2;
}
message WorkspaceGeneralSetting { message WorkspaceGeneralSetting {
// instance_url is the instance URL. // instance_url is the instance URL.
string instance_url = 1; string instance_url = 1;
...@@ -37,16 +45,28 @@ message WorkspaceGeneralSetting { ...@@ -37,16 +45,28 @@ message WorkspaceGeneralSetting {
string additional_script = 5; string additional_script = 5;
// additional_style is the additional style. // additional_style is the additional style.
string additional_style = 6; string additional_style = 6;
// custom_profile is the custom profile.
WorkspaceCustomProfile custom_profile = 4;
}
message WorkspaceCustomProfile {
string title = 1;
string description = 2;
string logo_url = 3;
string locale = 4;
string appearance = 5;
} }
message WorkspaceStorageSetting { message WorkspaceStorageSetting {
// storage_type is the storage type. // storage_type is the storage type.
StorageType storage_type = 1; StorageType storage_type = 1;
// The id of actived external storage.
optional int32 actived_external_storage_id = 2;
// The local storage path for STORAGE_TYPE_LOCAL. // The local storage path for STORAGE_TYPE_LOCAL.
// e.g. assets/{timestamp}_{filename} // e.g. assets/{timestamp}_{filename}
string local_storage_path = 2; string local_storage_path = 3;
// The max upload size in megabytes. // The max upload size in megabytes.
int64 upload_size_limit_mb = 3; int64 upload_size_limit_mb = 4;
enum StorageType { enum StorageType {
STORAGE_TYPE_UNSPECIFIED = 0; STORAGE_TYPE_UNSPECIFIED = 0;
......
...@@ -3,7 +3,6 @@ package integration ...@@ -3,7 +3,6 @@ package integration
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/json"
"fmt" "fmt"
"path/filepath" "path/filepath"
"slices" "slices"
...@@ -34,10 +33,10 @@ func NewTelegramHandler(store *store.Store) *TelegramHandler { ...@@ -34,10 +33,10 @@ func NewTelegramHandler(store *store.Store) *TelegramHandler {
} }
func (t *TelegramHandler) BotToken(ctx context.Context) string { func (t *TelegramHandler) BotToken(ctx context.Context) string {
if setting, err := t.store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{ if workspaceSetting, err := t.store.GetWorkspaceSettingV1(ctx, &store.FindWorkspaceSetting{
Name: apiv1.SystemSettingTelegramBotTokenName.String(), Name: storepb.WorkspaceSettingKey_WORKSPACE_SETTING_TELEGRAM_INTEGRATION.String(),
}); err == nil && setting != nil { }); err == nil && workspaceSetting != nil {
return setting.Value return workspaceSetting.GetTelegramIntegrationSetting().BotToken
} }
return "" return ""
} }
...@@ -170,21 +169,12 @@ func (t *TelegramHandler) CallbackQueryHandle(ctx context.Context, bot *telegram ...@@ -170,21 +169,12 @@ func (t *TelegramHandler) CallbackQueryHandle(ctx context.Context, bot *telegram
return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("Memo %d not found, possibly deleted elsewhere", memoID)) return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("Memo %d not found, possibly deleted elsewhere", memoID))
} }
setting, err := t.store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{ workspaceMemoRelatedSetting, err := t.store.GetWorkspaceMemoRelatedSetting(ctx)
Name: apiv1.SystemSettingDisablePublicMemosName.String(),
})
if err != nil { if err != nil {
return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("Failed to get workspace setting %s", err)) return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("Failed to get workspace setting %s", err))
} }
if setting != nil && setting.Value != "" { if workspaceMemoRelatedSetting.DisallowPublicVisible && visibility == store.Public {
disablePublicMemo := false return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("Failed to changing Memo %d to %s\n(workspace disallowed public memo)", memoID, visibility))
err = json.Unmarshal([]byte(setting.Value), &disablePublicMemo)
if err != nil {
return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("Failed to get workspace setting %s", err))
}
if disablePublicMemo && visibility == store.Public {
return bot.AnswerCallbackQuery(ctx, callbackQuery.ID, fmt.Sprintf("Failed to changing Memo %d to %s\n(workspace disallowed public memo)", memoID, visibility))
}
} }
update := store.UpdateMemo{ update := store.UpdateMemo{
......
...@@ -202,14 +202,6 @@ func (s *APIV1Service) GetMemoList(c echo.Context) error { ...@@ -202,14 +202,6 @@ func (s *APIV1Service) GetMemoList(c echo.Context) error {
find.Offset = &offset find.Offset = &offset
} }
memoDisplayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get memo display with updated ts setting value").SetInternal(err)
}
if memoDisplayWithUpdatedTs {
find.OrderByUpdatedTs = true
}
list, err := s.Store.ListMemos(ctx, find) list, err := s.Store.ListMemos(ctx, find)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch memo list").SetInternal(err)
...@@ -274,36 +266,6 @@ func (s *APIV1Service) CreateMemo(c echo.Context) error { ...@@ -274,36 +266,6 @@ func (s *APIV1Service) CreateMemo(c echo.Context) error {
} }
} }
// Find disable public memos system setting.
disablePublicMemosSystemSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{
Name: SystemSettingDisablePublicMemosName.String(),
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
}
if disablePublicMemosSystemSetting != nil {
disablePublicMemos := false
err = json.Unmarshal([]byte(disablePublicMemosSystemSetting.Value), &disablePublicMemos)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
}
if disablePublicMemos {
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
// Enforce normal user to create private memo if public memos are disabled.
if user.Role == store.RoleUser {
createMemoRequest.Visibility = Private
}
}
}
createMemoRequest.CreatorID = userID createMemoRequest.CreatorID = userID
memo, err := s.Store.CreateMemo(ctx, convertCreateMemoRequestToMemoMessage(createMemoRequest)) memo, err := s.Store.CreateMemo(ctx, convertCreateMemoRequestToMemoMessage(createMemoRequest))
if err != nil { if err != nil {
...@@ -444,14 +406,6 @@ func (s *APIV1Service) GetAllMemos(c echo.Context) error { ...@@ -444,14 +406,6 @@ func (s *APIV1Service) GetAllMemos(c echo.Context) error {
normalStatus := store.Normal normalStatus := store.Normal
memoFind.RowStatus = &normalStatus memoFind.RowStatus = &normalStatus
memoDisplayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get memo display with updated ts setting value").SetInternal(err)
}
if memoDisplayWithUpdatedTs {
memoFind.OrderByUpdatedTs = true
}
list, err := s.Store.ListMemos(ctx, memoFind) list, err := s.Store.ListMemos(ctx, memoFind)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch all memo list").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch all memo list").SetInternal(err)
...@@ -512,28 +466,14 @@ func (s *APIV1Service) GetMemoStats(c echo.Context) error { ...@@ -512,28 +466,14 @@ func (s *APIV1Service) GetMemoStats(c echo.Context) error {
} }
} }
memoDisplayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get memo display with updated ts setting value").SetInternal(err)
}
if memoDisplayWithUpdatedTs {
findMemoMessage.OrderByUpdatedTs = true
}
list, err := s.Store.ListMemos(ctx, findMemoMessage) list, err := s.Store.ListMemos(ctx, findMemoMessage)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)
} }
displayTsList := []int64{} displayTsList := []int64{}
if memoDisplayWithUpdatedTs { for _, memo := range list {
for _, memo := range list { displayTsList = append(displayTsList, memo.CreatedTs)
displayTsList = append(displayTsList, memo.UpdatedTs)
}
} else {
for _, memo := range list {
displayTsList = append(displayTsList, memo.CreatedTs)
}
} }
return c.JSON(http.StatusOK, displayTsList) return c.JSON(http.StatusOK, displayTsList)
} }
...@@ -704,40 +644,6 @@ func (s *APIV1Service) UpdateMemo(c echo.Context) error { ...@@ -704,40 +644,6 @@ func (s *APIV1Service) UpdateMemo(c echo.Context) error {
rowStatus := store.RowStatus(patchMemoRequest.RowStatus.String()) rowStatus := store.RowStatus(patchMemoRequest.RowStatus.String())
updateMemoMessage.RowStatus = &rowStatus updateMemoMessage.RowStatus = &rowStatus
} }
if patchMemoRequest.Visibility != nil {
visibility := store.Visibility(patchMemoRequest.Visibility.String())
updateMemoMessage.Visibility = &visibility
// Find disable public memos system setting.
disablePublicMemosSystemSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{
Name: SystemSettingDisablePublicMemosName.String(),
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
}
if disablePublicMemosSystemSetting != nil {
disablePublicMemos := false
err = json.Unmarshal([]byte(disablePublicMemosSystemSetting.Value), &disablePublicMemos)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
}
if disablePublicMemos {
user, err := s.Store.GetUser(ctx, &store.FindUser{
ID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err)
}
if user == nil {
return echo.NewHTTPError(http.StatusNotFound, "User not found")
}
// Enforce normal user to save as private memo if public memos are disabled.
if user.Role == store.RoleUser {
visibility = store.Visibility("PRIVATE")
updateMemoMessage.Visibility = &visibility
}
}
}
}
err = s.Store.UpdateMemo(ctx, updateMemoMessage) err = s.Store.UpdateMemo(ctx, updateMemoMessage)
if err != nil { if err != nil {
...@@ -853,14 +759,6 @@ func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Mem ...@@ -853,14 +759,6 @@ func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Mem
// Compose display ts. // Compose display ts.
memoMessage.DisplayTs = memoMessage.CreatedTs memoMessage.DisplayTs = memoMessage.CreatedTs
// Find memo display with updated ts setting.
memoDisplayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx)
if err != nil {
return nil, err
}
if memoDisplayWithUpdatedTs {
memoMessage.DisplayTs = memoMessage.UpdatedTs
}
// Compose related resources. // Compose related resources.
resourceList, err := s.Store.ListResources(ctx, &store.FindResource{ resourceList, err := s.Store.ListResources(ctx, &store.FindResource{
...@@ -898,23 +796,6 @@ func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Mem ...@@ -898,23 +796,6 @@ func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Mem
return memoMessage, nil return memoMessage, nil
} }
func (s *APIV1Service) getMemoDisplayWithUpdatedTsSettingValue(ctx context.Context) (bool, error) {
memoDisplayWithUpdatedTsSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{
Name: SystemSettingMemoDisplayWithUpdatedTsName.String(),
})
if err != nil {
return false, errors.Wrap(err, "failed to find system setting")
}
memoDisplayWithUpdatedTs := false
if memoDisplayWithUpdatedTsSetting != nil {
err = json.Unmarshal([]byte(memoDisplayWithUpdatedTsSetting.Value), &memoDisplayWithUpdatedTs)
if err != nil {
return false, errors.Wrap(err, "failed to unmarshal system setting value")
}
}
return memoDisplayWithUpdatedTs, nil
}
func convertCreateMemoRequestToMemoMessage(memoCreate *CreateMemoRequest) *store.Memo { func convertCreateMemoRequestToMemoMessage(memoCreate *CreateMemoRequest) *store.Memo {
createdTs := time.Now().Unix() createdTs := time.Now().Unix()
if memoCreate.CreatedTs != nil { if memoCreate.CreatedTs != nil {
......
...@@ -98,10 +98,6 @@ func (s *APIV1Service) GetSystemStatus(c echo.Context) error { ...@@ -98,10 +98,6 @@ func (s *APIV1Service) GetSystemStatus(c echo.Context) error {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting list").SetInternal(err)
} }
for _, systemSetting := range systemSettingList { for _, systemSetting := range systemSettingList {
if systemSetting.Name == SystemSettingServerIDName.String() || systemSetting.Name == SystemSettingSecretSessionName.String() || systemSetting.Name == SystemSettingTelegramBotTokenName.String() {
continue
}
var baseValue any var baseValue any
err := json.Unmarshal([]byte(systemSetting.Value), &baseValue) err := json.Unmarshal([]byte(systemSetting.Value), &baseValue)
if err != nil { if err != nil {
...@@ -110,8 +106,6 @@ func (s *APIV1Service) GetSystemStatus(c echo.Context) error { ...@@ -110,8 +106,6 @@ func (s *APIV1Service) GetSystemStatus(c echo.Context) error {
} }
switch systemSetting.Name { switch systemSetting.Name {
case SystemSettingDisablePublicMemosName.String():
systemStatus.DisablePublicMemos = baseValue.(bool)
case SystemSettingMaxUploadSizeMiBName.String(): case SystemSettingMaxUploadSizeMiBName.String():
systemStatus.MaxUploadSizeMiB = int(baseValue.(float64)) systemStatus.MaxUploadSizeMiB = int(baseValue.(float64))
case SystemSettingCustomizedProfileName.String(): case SystemSettingCustomizedProfileName.String():
...@@ -122,10 +116,6 @@ func (s *APIV1Service) GetSystemStatus(c echo.Context) error { ...@@ -122,10 +116,6 @@ func (s *APIV1Service) GetSystemStatus(c echo.Context) error {
systemStatus.CustomizedProfile = customizedProfile systemStatus.CustomizedProfile = customizedProfile
case SystemSettingStorageServiceIDName.String(): case SystemSettingStorageServiceIDName.String():
systemStatus.StorageServiceID = int32(baseValue.(float64)) systemStatus.StorageServiceID = int32(baseValue.(float64))
case SystemSettingLocalStoragePathName.String():
systemStatus.LocalStoragePath = baseValue.(string)
case SystemSettingMemoDisplayWithUpdatedTsName.String():
systemStatus.MemoDisplayWithUpdatedTs = baseValue.(bool)
default: default:
// Skip unknown system setting. // Skip unknown system setting.
} }
......
...@@ -15,12 +15,6 @@ import ( ...@@ -15,12 +15,6 @@ import (
type SystemSettingName string type SystemSettingName string
const ( const (
// SystemSettingServerIDName is the name of server id.
SystemSettingServerIDName SystemSettingName = "server-id"
// SystemSettingSecretSessionName is the name of secret session.
SystemSettingSecretSessionName SystemSettingName = "secret-session"
// SystemSettingDisablePublicMemosName is the name of disable public memos setting.
SystemSettingDisablePublicMemosName SystemSettingName = "disable-public-memos"
// SystemSettingMaxUploadSizeMiBName is the name of max upload size setting. // SystemSettingMaxUploadSizeMiBName is the name of max upload size setting.
SystemSettingMaxUploadSizeMiBName SystemSettingName = "max-upload-size-mib" SystemSettingMaxUploadSizeMiBName SystemSettingName = "max-upload-size-mib"
// SystemSettingCustomizedProfileName is the name of customized server profile. // SystemSettingCustomizedProfileName is the name of customized server profile.
...@@ -29,10 +23,6 @@ const ( ...@@ -29,10 +23,6 @@ const (
SystemSettingStorageServiceIDName SystemSettingName = "storage-service-id" SystemSettingStorageServiceIDName SystemSettingName = "storage-service-id"
// SystemSettingLocalStoragePathName is the name of local storage path. // SystemSettingLocalStoragePathName is the name of local storage path.
SystemSettingLocalStoragePathName SystemSettingName = "local-storage-path" SystemSettingLocalStoragePathName SystemSettingName = "local-storage-path"
// SystemSettingTelegramBotTokenName is the name of Telegram Bot Token.
SystemSettingTelegramBotTokenName SystemSettingName = "telegram-bot-token"
// SystemSettingMemoDisplayWithUpdatedTsName is the name of memo display with updated ts.
SystemSettingMemoDisplayWithUpdatedTsName SystemSettingName = "memo-display-with-updated-ts"
) )
const systemSettingUnmarshalError = `failed to unmarshal value from system setting "%v"` const systemSettingUnmarshalError = `failed to unmarshal value from system setting "%v"`
...@@ -160,13 +150,6 @@ func (s *APIV1Service) CreateSystemSetting(c echo.Context) error { ...@@ -160,13 +150,6 @@ func (s *APIV1Service) CreateSystemSetting(c echo.Context) error {
func (upsert UpsertSystemSettingRequest) Validate() error { func (upsert UpsertSystemSettingRequest) Validate() error {
switch settingName := upsert.Name; settingName { switch settingName := upsert.Name; settingName {
case SystemSettingServerIDName:
return errors.Errorf("updating %v is not allowed", settingName)
case SystemSettingDisablePublicMemosName:
var value bool
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
case SystemSettingMaxUploadSizeMiBName: case SystemSettingMaxUploadSizeMiBName:
var value int var value int
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil { if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
...@@ -213,27 +196,6 @@ func (upsert UpsertSystemSettingRequest) Validate() error { ...@@ -213,27 +196,6 @@ func (upsert UpsertSystemSettingRequest) Validate() error {
case !strings.Contains(trimmedValue, "{filename}"): case !strings.Contains(trimmedValue, "{filename}"):
return errors.New("local storage path must contain `{filename}`") return errors.New("local storage path must contain `{filename}`")
} }
case SystemSettingTelegramBotTokenName:
if upsert.Value == "" {
return nil
}
// Bot Token with Reverse Proxy shoule like `http.../bot<token>`
if strings.HasPrefix(upsert.Value, "http") {
slashIndex := strings.LastIndexAny(upsert.Value, "/")
if strings.HasPrefix(upsert.Value[slashIndex:], "/bot") {
return nil
}
return errors.New("token start with `http` must end with `/bot<token>`")
}
fragments := strings.Split(upsert.Value, ":")
if len(fragments) != 2 {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
case SystemSettingMemoDisplayWithUpdatedTsName:
var value bool
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
return errors.Errorf(systemSettingUnmarshalError, settingName)
}
default: default:
return errors.New("invalid system setting name") return errors.New("invalid system setting name")
} }
......
...@@ -446,20 +446,50 @@ paths: ...@@ -446,20 +446,50 @@ paths:
schema: schema:
$ref: '#/definitions/googlerpcStatus' $ref: '#/definitions/googlerpcStatus'
parameters: parameters:
- name: filename - name: resource.name
description: |-
The name of the resource.
Format: resources/{id}
id is the system generated unique identifier.
in: query in: query
required: false required: false
type: string type: string
- name: externalLink - name: resource.uid
description: The user defined id of the resource.
in: query in: query
required: false required: false
type: string type: string
- name: type - name: resource.createTime
in: query in: query
required: false required: false
type: string type: string
- name: memo format: date-time
description: 'Format: memos/{id}' - name: resource.filename
in: query
required: false
type: string
- name: resource.content
in: query
required: false
type: string
format: byte
- name: resource.externalLink
in: query
required: false
type: string
- name: resource.type
in: query
required: false
type: string
- name: resource.size
in: query
required: false
type: string
format: int64
- name: resource.memo
description: |-
The related memo.
Format: memos/{id}
in: query in: query
required: false required: false
type: string type: string
...@@ -1110,6 +1140,10 @@ paths: ...@@ -1110,6 +1140,10 @@ paths:
$ref: '#/definitions/googlerpcStatus' $ref: '#/definitions/googlerpcStatus'
parameters: parameters:
- name: name_2 - name: name_2
description: |-
The name of the resource.
Format: resources/{id}
id is the system generated unique identifier.
in: path in: path
required: true required: true
type: string type: string
...@@ -1177,6 +1211,10 @@ paths: ...@@ -1177,6 +1211,10 @@ paths:
$ref: '#/definitions/googlerpcStatus' $ref: '#/definitions/googlerpcStatus'
parameters: parameters:
- name: name_3 - name: name_3
description: |-
The name of the resource.
Format: resources/{id}
id is the system generated unique identifier.
in: path in: path
required: true required: true
type: string type: string
...@@ -1641,8 +1679,12 @@ paths: ...@@ -1641,8 +1679,12 @@ paths:
createTime: createTime:
type: string type: string
format: date-time format: date-time
readOnly: true
filename: filename:
type: string type: string
content:
type: string
format: byte
externalLink: externalLink:
type: string type: string
type: type:
...@@ -1652,7 +1694,9 @@ paths: ...@@ -1652,7 +1694,9 @@ paths:
format: int64 format: int64
memo: memo:
type: string type: string
title: 'Format: memos/{id}' title: |-
The related memo.
Format: memos/{id}
tags: tags:
- ResourceService - ResourceService
/api/v2/{setting.name}: /api/v2/{setting.name}:
...@@ -2472,8 +2516,12 @@ definitions: ...@@ -2472,8 +2516,12 @@ definitions:
createTime: createTime:
type: string type: string
format: date-time format: date-time
readOnly: true
filename: filename:
type: string type: string
content:
type: string
format: byte
externalLink: externalLink:
type: string type: string
type: type:
...@@ -2483,7 +2531,9 @@ definitions: ...@@ -2483,7 +2531,9 @@ definitions:
format: int64 format: int64
memo: memo:
type: string type: string
title: 'Format: memos/{id}' title: |-
The related memo.
Format: memos/{id}
v2SearchMemosResponse: v2SearchMemosResponse:
type: object type: object
properties: properties:
......
...@@ -4,7 +4,6 @@ import ( ...@@ -4,7 +4,6 @@ import (
"archive/zip" "archive/zip"
"bytes" "bytes"
"context" "context"
"encoding/json"
"fmt" "fmt"
"log/slog" "log/slog"
"time" "time"
...@@ -21,7 +20,6 @@ import ( ...@@ -21,7 +20,6 @@ import (
"github.com/usememos/memos/plugin/webhook" "github.com/usememos/memos/plugin/webhook"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2" apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
storepb "github.com/usememos/memos/proto/gen/store" storepb "github.com/usememos/memos/proto/gen/store"
apiv1 "github.com/usememos/memos/server/route/api/v1"
"github.com/usememos/memos/store" "github.com/usememos/memos/store"
) )
...@@ -49,12 +47,11 @@ func (s *APIV2Service) CreateMemo(ctx context.Context, request *apiv2pb.CreateMe ...@@ -49,12 +47,11 @@ func (s *APIV2Service) CreateMemo(ctx context.Context, request *apiv2pb.CreateMe
Content: request.Content, Content: request.Content,
Visibility: convertVisibilityToStore(request.Visibility), Visibility: convertVisibilityToStore(request.Visibility),
} }
// Find disable public memos system setting. workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
disablePublicMemosSystem, err := s.getDisablePublicMemosSystemSettingValue(ctx)
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get system setting") return nil, status.Errorf(codes.Internal, "failed to get workspace memo related setting")
} }
if disablePublicMemosSystem && create.Visibility == store.Public { if workspaceMemoRelatedSetting.DisallowPublicVisible && create.Visibility == store.Public {
return nil, status.Errorf(codes.PermissionDenied, "disable public memos system setting is enabled") return nil, status.Errorf(codes.PermissionDenied, "disable public memos system setting is enabled")
} }
...@@ -238,13 +235,12 @@ func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv2pb.UpdateMe ...@@ -238,13 +235,12 @@ func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv2pb.UpdateMe
return nil, status.Errorf(codes.InvalidArgument, "invalid resource name") return nil, status.Errorf(codes.InvalidArgument, "invalid resource name")
} }
} else if path == "visibility" { } else if path == "visibility" {
visibility := convertVisibilityToStore(request.Memo.Visibility) workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
// Find disable public memos system setting.
disablePublicMemosSystem, err := s.getDisablePublicMemosSystemSettingValue(ctx)
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get system setting") return nil, status.Errorf(codes.Internal, "failed to get workspace memo related setting")
} }
if disablePublicMemosSystem && visibility == store.Public { visibility := convertVisibilityToStore(request.Memo.Visibility)
if workspaceMemoRelatedSetting.DisallowPublicVisible && visibility == store.Public {
return nil, status.Errorf(codes.PermissionDenied, "disable public memos system setting is enabled") return nil, status.Errorf(codes.PermissionDenied, "disable public memos system setting is enabled")
} }
update.Visibility = &visibility update.Visibility = &visibility
...@@ -467,14 +463,14 @@ func (s *APIV2Service) GetUserMemosStats(ctx context.Context, request *apiv2pb.G ...@@ -467,14 +463,14 @@ func (s *APIV2Service) GetUserMemosStats(ctx context.Context, request *apiv2pb.G
return nil, status.Errorf(codes.Internal, "invalid timezone location") return nil, status.Errorf(codes.Internal, "invalid timezone location")
} }
displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx) workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value") return nil, status.Errorf(codes.Internal, "failed to get workspace memo related setting")
} }
stats := make(map[string]int32) stats := make(map[string]int32)
for _, memo := range memos { for _, memo := range memos {
displayTs := memo.CreatedTs displayTs := memo.CreatedTs
if displayWithUpdatedTs { if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
displayTs = memo.UpdatedTs displayTs = memo.UpdatedTs
} }
stats[time.Unix(displayTs, 0).In(location).Format("2006-01-02")]++ stats[time.Unix(displayTs, 0).In(location).Format("2006-01-02")]++
...@@ -529,7 +525,11 @@ func (s *APIV2Service) ExportMemos(ctx context.Context, request *apiv2pb.ExportM ...@@ -529,7 +525,11 @@ func (s *APIV2Service) ExportMemos(ctx context.Context, request *apiv2pb.ExportM
func (s *APIV2Service) convertMemoFromStore(ctx context.Context, memo *store.Memo) (*apiv2pb.Memo, error) { func (s *APIV2Service) convertMemoFromStore(ctx context.Context, memo *store.Memo) (*apiv2pb.Memo, error) {
displayTs := memo.CreatedTs displayTs := memo.CreatedTs
if displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx); err == nil && displayWithUpdatedTs { workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to get workspace memo related setting")
}
if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
displayTs = memo.UpdatedTs displayTs = memo.UpdatedTs
} }
...@@ -572,42 +572,6 @@ func (s *APIV2Service) convertMemoFromStore(ctx context.Context, memo *store.Mem ...@@ -572,42 +572,6 @@ func (s *APIV2Service) convertMemoFromStore(ctx context.Context, memo *store.Mem
}, nil }, nil
} }
func (s *APIV2Service) getMemoDisplayWithUpdatedTsSettingValue(ctx context.Context) (bool, error) {
memoDisplayWithUpdatedTsSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{
Name: apiv1.SystemSettingMemoDisplayWithUpdatedTsName.String(),
})
if err != nil {
return false, errors.Wrap(err, "failed to find system setting")
}
if memoDisplayWithUpdatedTsSetting == nil {
return false, nil
}
memoDisplayWithUpdatedTs := false
if err := json.Unmarshal([]byte(memoDisplayWithUpdatedTsSetting.Value), &memoDisplayWithUpdatedTs); err != nil {
return false, errors.Wrap(err, "failed to unmarshal system setting value")
}
return memoDisplayWithUpdatedTs, nil
}
func (s *APIV2Service) getDisablePublicMemosSystemSettingValue(ctx context.Context) (bool, error) {
disablePublicMemosSystemSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{
Name: apiv1.SystemSettingDisablePublicMemosName.String(),
})
if err != nil {
return false, errors.Wrap(err, "failed to find system setting")
}
if disablePublicMemosSystemSetting == nil {
return false, nil
}
disablePublicMemos := false
if err := json.Unmarshal([]byte(disablePublicMemosSystemSetting.Value), &disablePublicMemos); err != nil {
return false, errors.Wrap(err, "failed to unmarshal system setting value")
}
return disablePublicMemos, nil
}
func convertVisibilityFromStore(visibility store.Visibility) apiv2pb.Visibility { func convertVisibilityFromStore(visibility store.Visibility) apiv2pb.Visibility {
switch visibility { switch visibility {
case store.Private: case store.Private:
...@@ -654,22 +618,22 @@ func (s *APIV2Service) buildMemoFindWithFilter(ctx context.Context, find *store. ...@@ -654,22 +618,22 @@ func (s *APIV2Service) buildMemoFindWithFilter(ctx context.Context, find *store.
find.OrderByPinned = filter.OrderByPinned find.OrderByPinned = filter.OrderByPinned
} }
if filter.DisplayTimeAfter != nil { if filter.DisplayTimeAfter != nil {
displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx) workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
if err != nil { if err != nil {
return status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value") return status.Errorf(codes.Internal, "failed to get workspace memo related setting")
} }
if displayWithUpdatedTs { if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
find.UpdatedTsAfter = filter.DisplayTimeAfter find.UpdatedTsAfter = filter.DisplayTimeAfter
} else { } else {
find.CreatedTsAfter = filter.DisplayTimeAfter find.CreatedTsAfter = filter.DisplayTimeAfter
} }
} }
if filter.DisplayTimeBefore != nil { if filter.DisplayTimeBefore != nil {
displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx) workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
if err != nil { if err != nil {
return status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value") return status.Errorf(codes.Internal, "failed to get workspace memo related setting")
} }
if displayWithUpdatedTs { if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
find.UpdatedTsBefore = filter.DisplayTimeBefore find.UpdatedTsBefore = filter.DisplayTimeBefore
} else { } else {
find.CreatedTsBefore = filter.DisplayTimeBefore find.CreatedTsBefore = filter.DisplayTimeBefore
...@@ -717,11 +681,11 @@ func (s *APIV2Service) buildMemoFindWithFilter(ctx context.Context, find *store. ...@@ -717,11 +681,11 @@ func (s *APIV2Service) buildMemoFindWithFilter(ctx context.Context, find *store.
find.VisibilityList = []store.Visibility{store.Public, store.Protected} find.VisibilityList = []store.Visibility{store.Public, store.Protected}
} }
displayWithUpdatedTs, err := s.getMemoDisplayWithUpdatedTsSettingValue(ctx) workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx)
if err != nil { if err != nil {
return status.Errorf(codes.Internal, "failed to get memo display with updated ts setting value") return status.Errorf(codes.Internal, "failed to get workspace memo related setting")
} }
if displayWithUpdatedTs { if workspaceMemoRelatedSetting.DisplayWithUpdateTime {
find.OrderByUpdatedTs = true find.OrderByUpdatedTs = true
} }
return nil return nil
......
...@@ -15,6 +15,7 @@ const ( ...@@ -15,6 +15,7 @@ const (
MemoNamePrefix = "memos/" MemoNamePrefix = "memos/"
ResourceNamePrefix = "resources/" ResourceNamePrefix = "resources/"
InboxNamePrefix = "inboxes/" InboxNamePrefix = "inboxes/"
StorageNamePrefix = "storages/"
) )
// GetNameParentTokens returns the tokens from a resource name. // GetNameParentTokens returns the tokens from a resource name.
...@@ -96,3 +97,16 @@ func ExtractInboxIDFromName(name string) (int32, error) { ...@@ -96,3 +97,16 @@ func ExtractInboxIDFromName(name string) (int32, error) {
} }
return id, nil return id, nil
} }
// ExtractStorageIDFromName returns the storage ID from a resource name.
func ExtractStorageIDFromName(name string) (int32, error) {
tokens, err := GetNameParentTokens(name, StorageNamePrefix)
if err != nil {
return 0, err
}
id, err := util.ConvertStringToInt32(tokens[0])
if err != nil {
return 0, errors.Errorf("invalid storage ID %q", tokens[0])
}
return id, nil
}
package v2 package v2
import ( import (
"bytes"
"context" "context"
"encoding/binary"
"fmt" "fmt"
"net/url" "net/url"
"os"
"path/filepath"
"regexp"
"strings"
"time" "time"
"github.com/google/cel-go/cel" "github.com/google/cel-go/cel"
...@@ -14,35 +20,65 @@ import ( ...@@ -14,35 +20,65 @@ import (
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
"github.com/usememos/memos/internal/util"
"github.com/usememos/memos/plugin/storage/s3"
apiv2pb "github.com/usememos/memos/proto/gen/api/v2" apiv2pb "github.com/usememos/memos/proto/gen/api/v2"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/store" "github.com/usememos/memos/store"
) )
const (
// The upload memory buffer is 32 MiB.
// It should be kept low, so RAM usage doesn't get out of control.
// This is unrelated to maximum upload size limit, which is now set through system setting.
MaxUploadBufferSizeBytes = 32 << 20
MebiByte = 1024 * 1024
)
func (s *APIV2Service) CreateResource(ctx context.Context, request *apiv2pb.CreateResourceRequest) (*apiv2pb.CreateResourceResponse, error) { func (s *APIV2Service) CreateResource(ctx context.Context, request *apiv2pb.CreateResourceRequest) (*apiv2pb.CreateResourceResponse, error) {
user, err := getCurrentUser(ctx, s.Store) user, err := getCurrentUser(ctx, s.Store)
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err)
} }
if request.ExternalLink != "" {
create := &store.Resource{
UID: shortuuid.New(),
CreatorID: user.ID,
Filename: request.Resource.Filename,
Type: request.Resource.Type,
}
if request.Resource.ExternalLink != "" {
// Only allow those external links scheme with http/https // Only allow those external links scheme with http/https
linkURL, err := url.Parse(request.ExternalLink) linkURL, err := url.Parse(request.Resource.ExternalLink)
if err != nil { if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid external link: %v", err) return nil, status.Errorf(codes.InvalidArgument, "invalid external link: %v", err)
} }
if linkURL.Scheme != "http" && linkURL.Scheme != "https" { if linkURL.Scheme != "http" && linkURL.Scheme != "https" {
return nil, status.Errorf(codes.InvalidArgument, "invalid external link scheme: %v", linkURL.Scheme) return nil, status.Errorf(codes.InvalidArgument, "invalid external link scheme: %v", linkURL.Scheme)
} }
create.ExternalLink = request.Resource.ExternalLink
} else {
workspaceStorageSetting, err := s.Store.GetWorkspaceStorageSetting(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get workspace storage setting: %v", err)
}
size := binary.Size(request.Resource.Content)
uploadSizeLimit := int(workspaceStorageSetting.UploadSizeLimitMb) * MebiByte
if uploadSizeLimit == 0 {
uploadSizeLimit = MaxUploadBufferSizeBytes
}
if size > uploadSizeLimit {
return nil, status.Errorf(codes.InvalidArgument, "file size exceeds the limit")
}
create.Size = int64(size)
create.Blob = request.Resource.Content
if err := SaveResourceBlob(ctx, s.Store, create); err != nil {
return nil, status.Errorf(codes.Internal, "failed to save resource blob: %v", err)
}
} }
create := &store.Resource{ if request.Resource.Memo != nil {
UID: shortuuid.New(), memoID, err := ExtractMemoIDFromName(*request.Resource.Memo)
CreatorID: user.ID,
Filename: request.Filename,
ExternalLink: request.ExternalLink,
Type: request.Type,
}
if request.Memo != nil {
memoID, err := ExtractMemoIDFromName(*request.Memo)
if err != nil { if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "invalid memo id: %v", err) return nil, status.Errorf(codes.InvalidArgument, "invalid memo id: %v", err)
} }
...@@ -215,6 +251,126 @@ func (s *APIV2Service) convertResourceFromStore(ctx context.Context, resource *s ...@@ -215,6 +251,126 @@ func (s *APIV2Service) convertResourceFromStore(ctx context.Context, resource *s
return resourceMessage return resourceMessage
} }
// SaveResourceBlob save the blob of resource based on the storage config.
func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resource) error {
workspaceStorageSetting, err := s.GetWorkspaceStorageSetting(ctx)
if err != nil {
return errors.Wrap(err, "Failed to find workspace storage setting")
}
if workspaceStorageSetting.StorageType == storepb.WorkspaceStorageSetting_STORAGE_TYPE_LOCAL {
localStoragePath := "assets/{timestamp}_{filename}"
if workspaceStorageSetting.LocalStoragePath != "" {
localStoragePath = workspaceStorageSetting.LocalStoragePath
}
internalPath := localStoragePath
if !strings.Contains(internalPath, "{filename}") {
internalPath = filepath.Join(internalPath, "{filename}")
}
internalPath = replacePathTemplate(internalPath, create.Filename)
internalPath = filepath.ToSlash(internalPath)
osPath := filepath.FromSlash(internalPath)
if !filepath.IsAbs(osPath) {
osPath = filepath.Join(s.Profile.Data, osPath)
}
dir := filepath.Dir(osPath)
if err = os.MkdirAll(dir, os.ModePerm); err != nil {
return errors.Wrap(err, "Failed to create directory")
}
dst, err := os.Create(osPath)
if err != nil {
return errors.Wrap(err, "Failed to create file")
}
defer dst.Close()
if err := os.WriteFile(dir, create.Blob, 0644); err != nil {
return errors.Wrap(err, "Failed to write file")
}
create.InternalPath = internalPath
create.Blob = nil
} else if workspaceStorageSetting.StorageType == storepb.WorkspaceStorageSetting_STORAGE_TYPE_EXTERNAL {
if workspaceStorageSetting.ActivedExternalStorageId == nil {
return errors.Errorf("No actived external storage found")
}
storage, err := s.GetStorageV1(ctx, &store.FindStorage{ID: workspaceStorageSetting.ActivedExternalStorageId})
if err != nil {
return errors.Wrap(err, "Failed to find actived external storage")
}
if storage == nil {
return errors.Errorf("Storage %d not found", *workspaceStorageSetting.ActivedExternalStorageId)
}
if storage.Type != storepb.Storage_S3 {
return errors.Errorf("Unsupported storage type: %s", storage.Type.String())
}
s3Config := storage.Config.GetS3Config()
if s3Config == nil {
return errors.Errorf("S3 config not found")
}
s3Client, err := s3.NewClient(ctx, &s3.Config{
AccessKey: s3Config.AccessKey,
SecretKey: s3Config.SecretKey,
EndPoint: s3Config.EndPoint,
Region: s3Config.Region,
Bucket: s3Config.Bucket,
URLPrefix: s3Config.UrlPrefix,
URLSuffix: s3Config.UrlSuffix,
PreSign: s3Config.PreSign,
})
if err != nil {
return errors.Wrap(err, "Failed to create s3 client")
}
filePath := s3Config.Path
if !strings.Contains(filePath, "{filename}") {
filePath = filepath.Join(filePath, "{filename}")
}
filePath = replacePathTemplate(filePath, create.Filename)
r := bytes.NewReader(create.Blob)
link, err := s3Client.UploadFile(ctx, filePath, create.Type, r)
if err != nil {
return errors.Wrap(err, "Failed to upload via s3 client")
}
create.ExternalLink = link
create.Blob = nil
}
return nil
}
var fileKeyPattern = regexp.MustCompile(`\{[a-z]{1,9}\}`)
func replacePathTemplate(path, filename string) string {
t := time.Now()
path = fileKeyPattern.ReplaceAllStringFunc(path, func(s string) string {
switch s {
case "{filename}":
return filename
case "{timestamp}":
return fmt.Sprintf("%d", t.Unix())
case "{year}":
return fmt.Sprintf("%d", t.Year())
case "{month}":
return fmt.Sprintf("%02d", t.Month())
case "{day}":
return fmt.Sprintf("%02d", t.Day())
case "{hour}":
return fmt.Sprintf("%02d", t.Hour())
case "{minute}":
return fmt.Sprintf("%02d", t.Minute())
case "{second}":
return fmt.Sprintf("%02d", t.Second())
case "{uuid}":
return util.GenUUID()
}
return s
})
return path
}
// SearchResourcesFilterCELAttributes are the CEL attributes for SearchResourcesFilter. // SearchResourcesFilterCELAttributes are the CEL attributes for SearchResourcesFilter.
var SearchResourcesFilterCELAttributes = []cel.EnvOption{ var SearchResourcesFilterCELAttributes = []cel.EnvOption{
cel.Variable("uid", cel.StringType), cel.Variable("uid", cel.StringType),
......
...@@ -12,6 +12,7 @@ import ( ...@@ -12,6 +12,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/usememos/memos/plugin/telegram" "github.com/usememos/memos/plugin/telegram"
storepb "github.com/usememos/memos/proto/gen/store"
"github.com/usememos/memos/server/integration" "github.com/usememos/memos/server/integration"
"github.com/usememos/memos/server/profile" "github.com/usememos/memos/server/profile"
apiv1 "github.com/usememos/memos/server/route/api/v1" apiv1 "github.com/usememos/memos/server/route/api/v1"
...@@ -51,19 +52,16 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store ...@@ -51,19 +52,16 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store
// Register CORS middleware. // Register CORS middleware.
e.Use(CORSMiddleware(s.Profile.Origins)) e.Use(CORSMiddleware(s.Profile.Origins))
serverID, err := s.getSystemServerID(ctx) workspaceBasicSetting, err := s.getOrUpsertWorkspaceBasicSetting(ctx)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to retrieve system server ID") return nil, errors.Wrap(err, "failed to get workspace basic setting")
} }
s.ID = serverID
secret := "usememos" secret := "usememos"
if profile.Mode == "prod" { if profile.Mode == "prod" {
secret, err = s.getSystemSecretSessionName(ctx) secret = workspaceBasicSetting.SecretKey
if err != nil {
return nil, errors.Wrap(err, "failed to retrieve system secret session name")
}
} }
s.ID = workspaceBasicSetting.ServerId
s.Secret = secret s.Secret = secret
// Register healthz endpoint. // Register healthz endpoint.
...@@ -118,42 +116,31 @@ func (s *Server) GetEcho() *echo.Echo { ...@@ -118,42 +116,31 @@ func (s *Server) GetEcho() *echo.Echo {
return s.e return s.e
} }
func (s *Server) getSystemServerID(ctx context.Context) (string, error) { func (s *Server) getOrUpsertWorkspaceBasicSetting(ctx context.Context) (*storepb.WorkspaceBasicSetting, error) {
serverIDSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{ workspaceBasicSetting, err := s.Store.GetWorkspaceBasicSetting(ctx)
Name: apiv1.SystemSettingServerIDName.String(),
})
if err != nil { if err != nil {
return "", err return nil, errors.Wrap(err, "failed to get workspace basic setting")
} }
if serverIDSetting == nil || serverIDSetting.Value == "" { modified := false
serverIDSetting, err = s.Store.UpsertWorkspaceSetting(ctx, &store.WorkspaceSetting{ if workspaceBasicSetting.ServerId == "" {
Name: apiv1.SystemSettingServerIDName.String(), workspaceBasicSetting.ServerId = uuid.NewString()
Value: uuid.NewString(), modified = true
})
if err != nil {
return "", err
}
} }
return serverIDSetting.Value, nil if workspaceBasicSetting.SecretKey == "" {
} workspaceBasicSetting.SecretKey = uuid.NewString()
modified = true
func (s *Server) getSystemSecretSessionName(ctx context.Context) (string, error) {
secretSessionNameValue, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{
Name: apiv1.SystemSettingSecretSessionName.String(),
})
if err != nil {
return "", err
} }
if secretSessionNameValue == nil || secretSessionNameValue.Value == "" { if modified {
secretSessionNameValue, err = s.Store.UpsertWorkspaceSetting(ctx, &store.WorkspaceSetting{ workspaceSetting, err := s.Store.UpsertWorkspaceSettingV1(ctx, &storepb.WorkspaceSetting{
Name: apiv1.SystemSettingSecretSessionName.String(), Key: storepb.WorkspaceSettingKey_WORKSPACE_SETTING_BASIC,
Value: uuid.NewString(), Value: &storepb.WorkspaceSetting_BasicSetting{BasicSetting: workspaceBasicSetting},
}) })
if err != nil { if err != nil {
return "", err return nil, errors.Wrap(err, "failed to upsert workspace setting")
} }
workspaceBasicSetting = workspaceSetting.GetBasicSetting()
} }
return secretSessionNameValue.Value, nil return workspaceBasicSetting, nil
} }
func grpcRequestSkipper(c echo.Context) bool { func grpcRequestSkipper(c echo.Context) bool {
......
...@@ -17,6 +17,11 @@ func (s *Store) MigrateWorkspaceSetting(ctx context.Context) error { ...@@ -17,6 +17,11 @@ func (s *Store) MigrateWorkspaceSetting(ctx context.Context) error {
return errors.Wrap(err, "failed to list workspace settings") return errors.Wrap(err, "failed to list workspace settings")
} }
workspaceBasicSetting, err := s.GetWorkspaceBasicSetting(ctx)
if err != nil {
return errors.Wrap(err, "failed to get workspace basic setting")
}
workspaceGeneralSetting, err := s.GetWorkspaceGeneralSetting(ctx) workspaceGeneralSetting, err := s.GetWorkspaceGeneralSetting(ctx)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to get workspace general setting") return errors.Wrap(err, "failed to get workspace general setting")
...@@ -27,7 +32,11 @@ func (s *Store) MigrateWorkspaceSetting(ctx context.Context) error { ...@@ -27,7 +32,11 @@ func (s *Store) MigrateWorkspaceSetting(ctx context.Context) error {
var baseValue any var baseValue any
// nolint // nolint
json.Unmarshal([]byte(workspaceSetting.Value), &baseValue) json.Unmarshal([]byte(workspaceSetting.Value), &baseValue)
if workspaceSetting.Name == "allow-signup" { if workspaceSetting.Name == "server-id" {
workspaceBasicSetting.ServerId = workspaceSetting.Value
} else if workspaceSetting.Name == "secret-session" {
workspaceBasicSetting.SecretKey = workspaceSetting.Value
} else if workspaceSetting.Name == "allow-signup" {
workspaceGeneralSetting.DisallowSignup = !baseValue.(bool) workspaceGeneralSetting.DisallowSignup = !baseValue.(bool)
} else if workspaceSetting.Name == "disable-password-login" { } else if workspaceSetting.Name == "disable-password-login" {
workspaceGeneralSetting.DisallowPasswordLogin = baseValue.(bool) workspaceGeneralSetting.DisallowPasswordLogin = baseValue.(bool)
...@@ -50,6 +59,13 @@ func (s *Store) MigrateWorkspaceSetting(ctx context.Context) error { ...@@ -50,6 +59,13 @@ func (s *Store) MigrateWorkspaceSetting(ctx context.Context) error {
} }
} }
if _, err := s.UpsertWorkspaceSettingV1(ctx, &storepb.WorkspaceSetting{
Key: storepb.WorkspaceSettingKey_WORKSPACE_SETTING_BASIC,
Value: &storepb.WorkspaceSetting_BasicSetting{BasicSetting: workspaceBasicSetting},
}); err != nil {
return errors.Wrap(err, "failed to upsert workspace basic setting")
}
if _, err := s.UpsertWorkspaceSettingV1(ctx, &storepb.WorkspaceSetting{ if _, err := s.UpsertWorkspaceSettingV1(ctx, &storepb.WorkspaceSetting{
Key: storepb.WorkspaceSettingKey_WORKSPACE_SETTING_GENERAL, Key: storepb.WorkspaceSettingKey_WORKSPACE_SETTING_GENERAL,
Value: &storepb.WorkspaceSetting_GeneralSetting{GeneralSetting: workspaceGeneralSetting}, Value: &storepb.WorkspaceSetting_GeneralSetting{GeneralSetting: workspaceGeneralSetting},
......
...@@ -2,6 +2,10 @@ package store ...@@ -2,6 +2,10 @@ package store
import ( import (
"context" "context"
"github.com/pkg/errors"
storepb "github.com/usememos/memos/proto/gen/store"
"google.golang.org/protobuf/proto"
) )
type Storage struct { type Storage struct {
...@@ -52,3 +56,131 @@ func (s *Store) UpdateStorage(ctx context.Context, update *UpdateStorage) (*Stor ...@@ -52,3 +56,131 @@ func (s *Store) UpdateStorage(ctx context.Context, update *UpdateStorage) (*Stor
func (s *Store) DeleteStorage(ctx context.Context, delete *DeleteStorage) error { func (s *Store) DeleteStorage(ctx context.Context, delete *DeleteStorage) error {
return s.driver.DeleteStorage(ctx, delete) return s.driver.DeleteStorage(ctx, delete)
} }
func (s *Store) CreateStorageV1(ctx context.Context, create *storepb.Storage) (*storepb.Storage, error) {
storageRaw := &Storage{
Name: create.Name,
Type: create.Type.String(),
}
switch create.Type {
case storepb.Storage_S3:
configBytes, err := proto.Marshal(create.Config.GetS3Config())
if err != nil {
return nil, errors.Wrap(err, "failed to marshal s3 config")
}
storageRaw.Config = string(configBytes)
}
storageRaw, err := s.driver.CreateStorage(ctx, storageRaw)
if err != nil {
return nil, err
}
storage, err := convertStorageFromRaw(storageRaw)
if err != nil {
return nil, err
}
return storage, nil
}
func (s *Store) ListStoragesV1(ctx context.Context, find *FindStorage) ([]*storepb.Storage, error) {
list, err := s.driver.ListStorages(ctx, find)
if err != nil {
return nil, err
}
storages := []*storepb.Storage{}
for _, storageRaw := range list {
storage, err := convertStorageFromRaw(storageRaw)
if err != nil {
return nil, err
}
storages = append(storages, storage)
}
return storages, nil
}
func (s *Store) GetStorageV1(ctx context.Context, find *FindStorage) (*storepb.Storage, error) {
list, err := s.ListStoragesV1(ctx, find)
if err != nil {
return nil, err
}
if len(list) == 0 {
return nil, nil
}
return list[0], nil
}
type UpdateStorageV1 struct {
ID int32
Type storepb.Storage_Type
Name *string
Config *storepb.StorageConfig
}
func (s *Store) UpdateStorageV1(ctx context.Context, update *UpdateStorageV1) (*storepb.Storage, error) {
updateRaw := &UpdateStorage{
ID: update.ID,
}
if update.Name != nil {
updateRaw.Name = update.Name
}
if update.Config != nil {
configRaw, err := convertStorageConfigToRaw(update.Type, update.Config)
if err != nil {
return nil, err
}
updateRaw.Config = &configRaw
}
storageRaw, err := s.driver.UpdateStorage(ctx, updateRaw)
if err != nil {
return nil, err
}
storage, err := convertStorageFromRaw(storageRaw)
if err != nil {
return nil, err
}
return storage, nil
}
func convertStorageFromRaw(storageRaw *Storage) (*storepb.Storage, error) {
storage := &storepb.Storage{
Id: storageRaw.ID,
Name: storageRaw.Name,
Type: storepb.Storage_Type(storepb.Storage_Type_value[storageRaw.Type]),
}
storageConfig, err := convertStorageConfigFromRaw(storage.Type, storageRaw.Config)
if err != nil {
return nil, err
}
storage.Config = storageConfig
return storage, nil
}
func convertStorageConfigFromRaw(storageType storepb.Storage_Type, configRaw string) (*storepb.StorageConfig, error) {
storageConfig := &storepb.StorageConfig{}
switch storageType {
case storepb.Storage_S3:
s3Config := &storepb.S3Config{}
err := proto.Unmarshal([]byte(configRaw), s3Config)
if err != nil {
return nil, err
}
storageConfig.StorageConfig = &storepb.StorageConfig_S3Config{S3Config: s3Config}
}
return storageConfig, nil
}
func convertStorageConfigToRaw(storageType storepb.Storage_Type, config *storepb.StorageConfig) (string, error) {
raw := ""
switch storageType {
case storepb.Storage_S3:
bytes, err := proto.Marshal(config.GetS3Config())
if err != nil {
return "", err
}
raw = string(bytes)
}
return raw, nil
}
...@@ -75,7 +75,9 @@ func (s *Store) UpsertWorkspaceSettingV1(ctx context.Context, upsert *storepb.Wo ...@@ -75,7 +75,9 @@ func (s *Store) UpsertWorkspaceSettingV1(ctx context.Context, upsert *storepb.Wo
} }
var valueBytes []byte var valueBytes []byte
var err error var err error
if upsert.Key == storepb.WorkspaceSettingKey_WORKSPACE_SETTING_GENERAL { if upsert.Key == storepb.WorkspaceSettingKey_WORKSPACE_SETTING_BASIC {
valueBytes, err = protojson.Marshal(upsert.GetBasicSetting())
} else if upsert.Key == storepb.WorkspaceSettingKey_WORKSPACE_SETTING_GENERAL {
valueBytes, err = protojson.Marshal(upsert.GetGeneralSetting()) valueBytes, err = protojson.Marshal(upsert.GetGeneralSetting())
} else if upsert.Key == storepb.WorkspaceSettingKey_WORKSPACE_SETTING_STORAGE { } else if upsert.Key == storepb.WorkspaceSettingKey_WORKSPACE_SETTING_STORAGE {
valueBytes, err = protojson.Marshal(upsert.GetStorageSetting()) valueBytes, err = protojson.Marshal(upsert.GetStorageSetting())
...@@ -139,6 +141,21 @@ func (s *Store) GetWorkspaceSettingV1(ctx context.Context, find *FindWorkspaceSe ...@@ -139,6 +141,21 @@ func (s *Store) GetWorkspaceSettingV1(ctx context.Context, find *FindWorkspaceSe
return list[0], nil return list[0], nil
} }
func (s *Store) GetWorkspaceBasicSetting(ctx context.Context) (*storepb.WorkspaceBasicSetting, error) {
workspaceSetting, err := s.GetWorkspaceSettingV1(ctx, &FindWorkspaceSetting{
Name: storepb.WorkspaceSettingKey_WORKSPACE_SETTING_BASIC.String(),
})
if err != nil {
return nil, errors.Wrap(err, "failed to get workspace basic setting")
}
workspaceBasicSetting := &storepb.WorkspaceBasicSetting{}
if workspaceSetting != nil {
workspaceBasicSetting = workspaceSetting.GetBasicSetting()
}
return workspaceBasicSetting, nil
}
func (s *Store) GetWorkspaceGeneralSetting(ctx context.Context) (*storepb.WorkspaceGeneralSetting, error) { func (s *Store) GetWorkspaceGeneralSetting(ctx context.Context) (*storepb.WorkspaceGeneralSetting, error) {
workspaceSetting, err := s.GetWorkspaceSettingV1(ctx, &FindWorkspaceSetting{ workspaceSetting, err := s.GetWorkspaceSettingV1(ctx, &FindWorkspaceSetting{
Name: storepb.WorkspaceSettingKey_WORKSPACE_SETTING_GENERAL.String(), Name: storepb.WorkspaceSettingKey_WORKSPACE_SETTING_GENERAL.String(),
...@@ -154,11 +171,47 @@ func (s *Store) GetWorkspaceGeneralSetting(ctx context.Context) (*storepb.Worksp ...@@ -154,11 +171,47 @@ func (s *Store) GetWorkspaceGeneralSetting(ctx context.Context) (*storepb.Worksp
return workspaceGeneralSetting, nil return workspaceGeneralSetting, nil
} }
func (s *Store) GetWorkspaceMemoRelatedSetting(ctx context.Context) (*storepb.WorkspaceMemoRelatedSetting, error) {
workspaceSetting, err := s.GetWorkspaceSettingV1(ctx, &FindWorkspaceSetting{
Name: storepb.WorkspaceSettingKey_WORKSPACE_SETTING_MEMO_RELATED.String(),
})
if err != nil {
return nil, errors.Wrap(err, "failed to get workspace general setting")
}
workspaceMemoRelatedSetting := &storepb.WorkspaceMemoRelatedSetting{}
if workspaceSetting != nil {
workspaceMemoRelatedSetting = workspaceSetting.GetMemoRelatedSetting()
}
return workspaceMemoRelatedSetting, nil
}
func (s *Store) GetWorkspaceStorageSetting(ctx context.Context) (*storepb.WorkspaceStorageSetting, error) {
workspaceSetting, err := s.GetWorkspaceSettingV1(ctx, &FindWorkspaceSetting{
Name: storepb.WorkspaceSettingKey_WORKSPACE_SETTING_STORAGE.String(),
})
if err != nil {
return nil, errors.Wrap(err, "failed to get workspace storage setting")
}
workspaceStorageSetting := &storepb.WorkspaceStorageSetting{}
if workspaceSetting != nil {
workspaceStorageSetting = workspaceSetting.GetStorageSetting()
}
return workspaceStorageSetting, nil
}
func convertWorkspaceSettingFromRaw(workspaceSettingRaw *WorkspaceSetting) (*storepb.WorkspaceSetting, error) { func convertWorkspaceSettingFromRaw(workspaceSettingRaw *WorkspaceSetting) (*storepb.WorkspaceSetting, error) {
workspaceSetting := &storepb.WorkspaceSetting{ workspaceSetting := &storepb.WorkspaceSetting{
Key: storepb.WorkspaceSettingKey(storepb.WorkspaceSettingKey_value[workspaceSettingRaw.Name]), Key: storepb.WorkspaceSettingKey(storepb.WorkspaceSettingKey_value[workspaceSettingRaw.Name]),
} }
switch workspaceSettingRaw.Name { switch workspaceSettingRaw.Name {
case storepb.WorkspaceSettingKey_WORKSPACE_SETTING_BASIC.String():
basicSetting := &storepb.WorkspaceBasicSetting{}
if err := protojson.Unmarshal([]byte(workspaceSettingRaw.Value), basicSetting); err != nil {
return nil, err
}
workspaceSetting.Value = &storepb.WorkspaceSetting_BasicSetting{BasicSetting: basicSetting}
case storepb.WorkspaceSettingKey_WORKSPACE_SETTING_GENERAL.String(): case storepb.WorkspaceSettingKey_WORKSPACE_SETTING_GENERAL.String():
generalSetting := &storepb.WorkspaceGeneralSetting{} generalSetting := &storepb.WorkspaceGeneralSetting{}
if err := protojson.Unmarshal([]byte(workspaceSettingRaw.Value), generalSetting); err != nil { if err := protojson.Unmarshal([]byte(workspaceSettingRaw.Value), generalSetting); err != nil {
...@@ -183,6 +236,8 @@ func convertWorkspaceSettingFromRaw(workspaceSettingRaw *WorkspaceSetting) (*sto ...@@ -183,6 +236,8 @@ func convertWorkspaceSettingFromRaw(workspaceSettingRaw *WorkspaceSetting) (*sto
return nil, err return nil, err
} }
workspaceSetting.Value = &storepb.WorkspaceSetting_TelegramIntegrationSetting{TelegramIntegrationSetting: telegramIntegrationSetting} workspaceSetting.Value = &storepb.WorkspaceSetting_TelegramIntegrationSetting{TelegramIntegrationSetting: telegramIntegrationSetting}
default:
return nil, errors.Errorf("unsupported workspace setting key: %v", workspaceSettingRaw.Name)
} }
return workspaceSetting, nil return workspaceSetting, nil
} }
...@@ -60,7 +60,7 @@ const ChangeMemberPasswordDialog: React.FC<Props> = (props: Props) => { ...@@ -60,7 +60,7 @@ const ChangeMemberPasswordDialog: React.FC<Props> = (props: Props) => {
handleCloseBtnClick(); handleCloseBtnClick();
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
toast.error(error.response.data.message); toast.error(error.details);
} }
}; };
......
import { Autocomplete, Button, IconButton, Input, List, ListItem, Option, Select, Typography } from "@mui/joy"; import { Autocomplete, Button, IconButton, Input, List, ListItem, Option, Select, Typography } from "@mui/joy";
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { CreateResourceRequest, Resource } from "@/types/proto/api/v2/resource_service"; import { useResourceStore } from "@/store/v1";
import { Resource } from "@/types/proto/api/v2/resource_service";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
import { useResourceStore } from "../store/module";
import { generateDialog } from "./Dialog"; import { generateDialog } from "./Dialog";
import Icon from "./Icon"; import Icon from "./Icon";
...@@ -29,8 +29,8 @@ const CreateResourceDialog: React.FC<Props> = (props: Props) => { ...@@ -29,8 +29,8 @@ const CreateResourceDialog: React.FC<Props> = (props: Props) => {
selectedMode: "local-file", selectedMode: "local-file",
uploadingFlag: false, uploadingFlag: false,
}); });
const [resourceCreate, setResourceCreate] = useState<CreateResourceRequest>( const [resourceCreate, setResourceCreate] = useState<Resource>(
CreateResourceRequest.fromPartial({ Resource.fromPartial({
filename: "", filename: "",
externalLink: "", externalLink: "",
type: "", type: "",
...@@ -160,16 +160,25 @@ const CreateResourceDialog: React.FC<Props> = (props: Props) => { ...@@ -160,16 +160,25 @@ const CreateResourceDialog: React.FC<Props> = (props: Props) => {
if (!fileOnInput) { if (!fileOnInput) {
continue; continue;
} }
const resource = await resourceStore.createResourceWithBlob(file); const { name: filename, size, type } = file;
const buffer = new Uint8Array(await file.arrayBuffer());
const resource = await resourceStore.createResource({
resource: Resource.fromPartial({
filename,
size,
type,
content: buffer,
}),
});
createdResourceList.push(resource); createdResourceList.push(resource);
} }
} else { } else {
const resource = await resourceStore.createResource(resourceCreate); const resource = await resourceStore.createResource({ resource: resourceCreate });
createdResourceList.push(resource); createdResourceList.push(resource);
} }
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
toast.error(typeof error === "string" ? error : error.response.data.message); toast.error(error.details);
} }
if (onConfirm) { if (onConfirm) {
......
...@@ -7,8 +7,8 @@ import { memoServiceClient } from "@/grpcweb"; ...@@ -7,8 +7,8 @@ import { memoServiceClient } from "@/grpcweb";
import { TAB_SPACE_WIDTH } from "@/helpers/consts"; import { TAB_SPACE_WIDTH } from "@/helpers/consts";
import { isValidUrl } from "@/helpers/utils"; import { isValidUrl } from "@/helpers/utils";
import useCurrentUser from "@/hooks/useCurrentUser"; import useCurrentUser from "@/hooks/useCurrentUser";
import { useGlobalStore, useResourceStore, useTagStore } from "@/store/module"; import { useGlobalStore, useTagStore } from "@/store/module";
import { useMemoStore, useUserStore } from "@/store/v1"; import { useMemoStore, useResourceStore, useUserStore } from "@/store/v1";
import { MemoRelation, MemoRelation_Type } from "@/types/proto/api/v2/memo_relation_service"; import { MemoRelation, MemoRelation_Type } from "@/types/proto/api/v2/memo_relation_service";
import { Memo, Visibility } from "@/types/proto/api/v2/memo_service"; import { Memo, Visibility } from "@/types/proto/api/v2/memo_service";
import { Resource } from "@/types/proto/api/v2/resource_service"; import { Resource } from "@/types/proto/api/v2/resource_service";
...@@ -207,21 +207,29 @@ const MemoEditor = (props: Props) => { ...@@ -207,21 +207,29 @@ const MemoEditor = (props: Props) => {
}; };
}); });
let resource = undefined; const { name: filename, size, type } = file;
const buffer = new Uint8Array(await file.arrayBuffer());
try { try {
resource = await resourceStore.createResourceWithBlob(file); const resource = await resourceStore.createResource({
resource: Resource.fromPartial({
filename,
size,
type,
content: buffer,
}),
});
setState((state) => {
return {
...state,
isUploadingResource: false,
};
});
return resource;
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
toast.error(typeof error === "string" ? error : error.response.data.message); toast.error(error.details);
} }
setState((state) => {
return {
...state,
isUploadingResource: false,
};
});
return resource;
}; };
const uploadMultiFiles = async (files: FileList) => { const uploadMultiFiles = async (files: FileList) => {
......
import axios from "axios"; import axios from "axios";
import { Resource } from "@/types/proto/api/v2/resource_service";
axios.defaults.baseURL = import.meta.env.VITE_API_BASE_URL || ""; axios.defaults.baseURL = import.meta.env.VITE_API_BASE_URL || "";
axios.defaults.withCredentials = true; axios.defaults.withCredentials = true;
...@@ -16,10 +15,6 @@ export function upsertSystemSetting(systemSetting: SystemSetting) { ...@@ -16,10 +15,6 @@ export function upsertSystemSetting(systemSetting: SystemSetting) {
return axios.post<SystemSetting>("/api/v1/system/setting", systemSetting); return axios.post<SystemSetting>("/api/v1/system/setting", systemSetting);
} }
export function createResourceWithBlob(formData: FormData) {
return axios.post<Resource>("/api/v1/resource/blob", formData);
}
export function getStorageList() { export function getStorageList() {
return axios.get<ObjectStorage[]>(`/api/v1/storage`); return axios.get<ObjectStorage[]>(`/api/v1/storage`);
} }
......
...@@ -15,24 +15,23 @@ import { Resource } from "@/types/proto/api/v2/resource_service"; ...@@ -15,24 +15,23 @@ import { Resource } from "@/types/proto/api/v2/resource_service";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
function groupResourcesByDate(resources: Resource[]) { function groupResourcesByDate(resources: Resource[]) {
const tmp_resources: Resource[] = resources.slice();
tmp_resources.sort((a: Resource, b: Resource) => {
const a_date = new Date(a.createTime as any);
const b_date = new Date(b.createTime as any);
return b_date.getTime() - a_date.getTime();
});
const grouped = new Map<number, Resource[]>(); const grouped = new Map<number, Resource[]>();
tmp_resources.forEach((item) => { resources
const date = new Date(item.createTime as any); .sort((a: Resource, b: Resource) => {
const year = date.getFullYear(); const a_date = new Date(a.createTime as any);
const month = date.getMonth() + 1; const b_date = new Date(b.createTime as any);
const timestamp = Date.UTC(year, month - 1, 1); return b_date.getTime() - a_date.getTime();
if (!grouped.has(timestamp)) { })
grouped.set(timestamp, []); .forEach((item) => {
} const date = new Date(item.createTime as any);
grouped.get(timestamp)?.push(item); const year = date.getFullYear();
}); const month = date.getMonth() + 1;
const timestamp = Date.UTC(year, month - 1, 1);
if (!grouped.has(timestamp)) {
grouped.set(timestamp, []);
}
grouped.get(timestamp)?.push(item);
});
return grouped; return grouped;
} }
......
export * from "./global"; export * from "./global";
export * from "./filter"; export * from "./filter";
export * from "./tag"; export * from "./tag";
export * from "./resource";
export * from "./dialog"; export * from "./dialog";
import { resourceServiceClient } from "@/grpcweb";
import * as api from "@/helpers/api";
import { CreateResourceRequest, Resource, UpdateResourceRequest } from "@/types/proto/api/v2/resource_service";
import { useTranslate } from "@/utils/i18n";
import store, { useAppSelector } from "../";
import { patchResource, setResources } from "../reducer/resource";
import { useGlobalStore } from "./global";
export const useResourceStore = () => {
const state = useAppSelector((state) => state.resource);
const t = useTranslate();
const globalStore = useGlobalStore();
const maxUploadSizeMiB = globalStore.state.systemStatus.maxUploadSizeMiB;
return {
state,
getState: () => {
return store.getState().resource;
},
async createResource(create: CreateResourceRequest): Promise<Resource> {
const { resource } = await resourceServiceClient.createResource(create);
if (!resource) {
throw new Error("resource is null");
}
const resourceList = state.resources;
store.dispatch(setResources([resource, ...resourceList]));
return resource;
},
async createResourceWithBlob(file: File): Promise<Resource> {
const { name: filename, size } = file;
if (size > maxUploadSizeMiB * 1024 * 1024) {
return Promise.reject(t("message.maximum-upload-size-is", { size: maxUploadSizeMiB }));
}
const formData = new FormData();
formData.append("file", file, filename);
const { data: resource } = await api.createResourceWithBlob(formData);
const resourceList = state.resources;
store.dispatch(setResources([resource, ...resourceList]));
return resource;
},
async updateResource(update: UpdateResourceRequest): Promise<Resource> {
const { resource } = await resourceServiceClient.updateResource(update);
if (!resource) {
throw new Error("resource is null");
}
store.dispatch(patchResource(resource));
return resource;
},
};
};
import { create } from "zustand"; import { create } from "zustand";
import { combine } from "zustand/middleware"; import { combine } from "zustand/middleware";
import { resourceServiceClient } from "@/grpcweb"; import { resourceServiceClient } from "@/grpcweb";
import { Resource } from "@/types/proto/api/v2/resource_service"; import { CreateResourceRequest, Resource, UpdateResourceRequest } from "@/types/proto/api/v2/resource_service";
interface State { interface State {
resourceMapByName: Record<string, Resource>; resourceMapByName: Record<string, Resource>;
...@@ -30,5 +30,23 @@ export const useResourceStore = create( ...@@ -30,5 +30,23 @@ export const useResourceStore = create(
const resourceMap = get().resourceMapByName; const resourceMap = get().resourceMapByName;
return Object.values(resourceMap).find((r) => r.name === name); return Object.values(resourceMap).find((r) => r.name === name);
}, },
async createResource(create: CreateResourceRequest): Promise<Resource> {
const { resource } = await resourceServiceClient.createResource(create);
if (!resource) {
throw new Error("resource is null");
}
const resourceMap = get().resourceMapByName;
resourceMap[resource.name] = resource;
return resource;
},
async updateResource(update: UpdateResourceRequest): Promise<Resource> {
const { resource } = await resourceServiceClient.updateResource(update);
if (!resource) {
throw new Error("resource is null");
}
const resourceMap = get().resourceMapByName;
resourceMap[resource.name] = resource;
return resource;
},
})), })),
); );
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment