Skip to content

Commit 96021e5

Browse files
feat: add max upload size setting to UI & UI improvements (usememos#1646)
* Add preliminar Windows support for both development and production environments. Default profile.Data will be set to "C:\ProgramData\memos" on Windows. Folder will be created if it does not exist, as this behavior is expected for Windows applications. System service installation can be achieved with third-party tools, explained in docs/windows-service.md. Not sure if it's worth using https://github.com/kardianos/service to make service support built-in. This could be a nice addition alongside usememos#1583 (add Windows artifacts) * feat: improve Windows support - Fix local file storage path handling on Windows - Improve Windows dev script * feat: add max upload size setting to UI & more - feat: add max upload size setting to UI - feat: max upload size setting is checked on UI during upload, but also enforced by the server - fix: overflowing mobile layout for Create SSO, Create Storage and other Settings dialogs - feat: add HelpButton component with some links to docs were appropriate - remove LearnMore component in favor of HelpButton - refactor: change some if/else to switch statements - refactor: inline some err == nil checks ! Existing databases without the new setting 'max-upload-size-mib' will show an upload error, but this can be user-fixed by simply setting the value on system settings UI. * improvements requested by @boojack
1 parent 5c51999 commit 96021e5

20 files changed

Lines changed: 591 additions & 204 deletions

api/system.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ type SystemStatus struct {
1414
IgnoreUpgrade bool `json:"ignoreUpgrade"`
1515
// Disable public memos.
1616
DisablePublicMemos bool `json:"disablePublicMemos"`
17+
// Max upload size.
18+
MaxUploadSizeMiB int `json:"maxUploadSizeMiB"`
1719
// Additional style.
1820
AdditionalStyle string `json:"additionalStyle"`
1921
// Additional script.

api/system_setting.go

Lines changed: 61 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package api
22

33
import (
44
"encoding/json"
5-
"errors"
65
"fmt"
76

87
"golang.org/x/exp/slices"
@@ -21,6 +20,8 @@ const (
2120
SystemSettingIgnoreUpgradeName SystemSettingName = "ignore-upgrade"
2221
// SystemSettingDisablePublicMemosName is the name of disable public memos setting.
2322
SystemSettingDisablePublicMemosName SystemSettingName = "disable-public-memos"
23+
// SystemSettingMaxUploadSizeMiBName is the name of max upload size setting.
24+
SystemSettingMaxUploadSizeMiBName SystemSettingName = "max-upload-size-mib"
2425
// SystemSettingAdditionalStyleName is the name of additional style.
2526
SystemSettingAdditionalStyleName SystemSettingName = "additional-style"
2627
// SystemSettingAdditionalScriptName is the name of additional script.
@@ -68,6 +69,8 @@ func (key SystemSettingName) String() string {
6869
return "ignore-upgrade"
6970
case SystemSettingDisablePublicMemosName:
7071
return "disable-public-memos"
72+
case SystemSettingMaxUploadSizeMiBName:
73+
return "max-upload-size-mib"
7174
case SystemSettingAdditionalStyleName:
7275
return "additional-style"
7376
case SystemSettingAdditionalScriptName:
@@ -97,40 +100,50 @@ type SystemSettingUpsert struct {
97100
Description string `json:"description"`
98101
}
99102

103+
const systemSettingUnmarshalError = `failed to unmarshal value from system setting "%v"`
104+
100105
func (upsert SystemSettingUpsert) Validate() error {
101-
if upsert.Name == SystemSettingServerIDName {
102-
return errors.New("update server id is not allowed")
103-
} else if upsert.Name == SystemSettingAllowSignUpName {
104-
value := false
105-
err := json.Unmarshal([]byte(upsert.Value), &value)
106-
if err != nil {
107-
return fmt.Errorf("failed to unmarshal system setting allow signup value")
106+
switch settingName := upsert.Name; settingName {
107+
case SystemSettingServerIDName:
108+
return fmt.Errorf("updating %v is not allowed", settingName)
109+
110+
case SystemSettingAllowSignUpName:
111+
var value bool
112+
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
113+
return fmt.Errorf(systemSettingUnmarshalError, settingName)
108114
}
109-
} else if upsert.Name == SystemSettingIgnoreUpgradeName {
110-
value := false
111-
err := json.Unmarshal([]byte(upsert.Value), &value)
112-
if err != nil {
113-
return fmt.Errorf("failed to unmarshal system setting ignore upgrade value")
115+
116+
case SystemSettingIgnoreUpgradeName:
117+
var value bool
118+
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
119+
return fmt.Errorf(systemSettingUnmarshalError, settingName)
114120
}
115-
} else if upsert.Name == SystemSettingDisablePublicMemosName {
116-
value := false
117-
err := json.Unmarshal([]byte(upsert.Value), &value)
118-
if err != nil {
119-
return fmt.Errorf("failed to unmarshal system setting disable public memos value")
121+
122+
case SystemSettingDisablePublicMemosName:
123+
var value bool
124+
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
125+
return fmt.Errorf(systemSettingUnmarshalError, settingName)
120126
}
121-
} else if upsert.Name == SystemSettingAdditionalStyleName {
122-
value := ""
123-
err := json.Unmarshal([]byte(upsert.Value), &value)
124-
if err != nil {
125-
return fmt.Errorf("failed to unmarshal system setting additional style value")
127+
128+
case SystemSettingMaxUploadSizeMiBName:
129+
var value int
130+
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
131+
return fmt.Errorf(systemSettingUnmarshalError, settingName)
126132
}
127-
} else if upsert.Name == SystemSettingAdditionalScriptName {
128-
value := ""
129-
err := json.Unmarshal([]byte(upsert.Value), &value)
130-
if err != nil {
131-
return fmt.Errorf("failed to unmarshal system setting additional script value")
133+
134+
case SystemSettingAdditionalStyleName:
135+
var value string
136+
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
137+
return fmt.Errorf(systemSettingUnmarshalError, settingName)
132138
}
133-
} else if upsert.Name == SystemSettingCustomizedProfileName {
139+
140+
case SystemSettingAdditionalScriptName:
141+
var value string
142+
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
143+
return fmt.Errorf(systemSettingUnmarshalError, settingName)
144+
}
145+
146+
case SystemSettingCustomizedProfileName:
134147
customizedProfile := CustomizedProfile{
135148
Name: "memos",
136149
LogoURL: "",
@@ -139,36 +152,37 @@ func (upsert SystemSettingUpsert) Validate() error {
139152
Appearance: "system",
140153
ExternalURL: "",
141154
}
142-
err := json.Unmarshal([]byte(upsert.Value), &customizedProfile)
143-
if err != nil {
144-
return fmt.Errorf("failed to unmarshal system setting customized profile value")
155+
156+
if err := json.Unmarshal([]byte(upsert.Value), &customizedProfile); err != nil {
157+
return fmt.Errorf(systemSettingUnmarshalError, settingName)
145158
}
146159
if !slices.Contains(UserSettingLocaleValue, customizedProfile.Locale) {
147-
return fmt.Errorf("invalid locale value")
160+
return fmt.Errorf(`invalid locale value for system setting "%v"`, settingName)
148161
}
149162
if !slices.Contains(UserSettingAppearanceValue, customizedProfile.Appearance) {
150-
return fmt.Errorf("invalid appearance value")
163+
return fmt.Errorf(`invalid appearance value for system setting "%v"`, settingName)
151164
}
152-
} else if upsert.Name == SystemSettingStorageServiceIDName {
165+
166+
case SystemSettingStorageServiceIDName:
153167
value := DatabaseStorage
154-
err := json.Unmarshal([]byte(upsert.Value), &value)
155-
if err != nil {
156-
return fmt.Errorf("failed to unmarshal system setting storage service id value")
168+
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
169+
return fmt.Errorf(systemSettingUnmarshalError, settingName)
157170
}
158171
return nil
159-
} else if upsert.Name == SystemSettingLocalStoragePathName {
172+
173+
case SystemSettingLocalStoragePathName:
160174
value := ""
161-
err := json.Unmarshal([]byte(upsert.Value), &value)
162-
if err != nil {
163-
return fmt.Errorf("failed to unmarshal system setting local storage path value")
175+
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
176+
return fmt.Errorf(systemSettingUnmarshalError, settingName)
164177
}
165-
} else if upsert.Name == SystemSettingOpenAIConfigName {
178+
179+
case SystemSettingOpenAIConfigName:
166180
value := OpenAIConfig{}
167-
err := json.Unmarshal([]byte(upsert.Value), &value)
168-
if err != nil {
169-
return fmt.Errorf("failed to unmarshal system setting openai api config value")
181+
if err := json.Unmarshal([]byte(upsert.Value), &value); err != nil {
182+
return fmt.Errorf(systemSettingUnmarshalError, settingName)
170183
}
171-
} else {
184+
185+
default:
172186
return fmt.Errorf("invalid system setting name")
173187
}
174188

server/resource.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ import (
2525
)
2626

2727
const (
28-
// The max file size is 32MB.
29-
maxFileSize = 32 << 20
28+
// The upload memory buffer is 32 MiB.
29+
// It should be kept low, so RAM usage doesn't get out of control.
30+
// This is unrelated to maximum upload size limit, which is now set through system setting.
31+
maxUploadBufferSizeBytes = 32 << 20
32+
MebiByte = 1024 * 1024
3033
)
3134

3235
var fileKeyPattern = regexp.MustCompile(`\{[a-z]{1,9}\}`)
@@ -67,8 +70,13 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
6770
return echo.NewHTTPError(http.StatusUnauthorized, "Missing user in session")
6871
}
6972

70-
if err := c.Request().ParseMultipartForm(maxFileSize); err != nil {
71-
return echo.NewHTTPError(http.StatusBadRequest, "Upload file overload max size").SetInternal(err)
73+
maxUploadSetting := s.Store.GetSystemSettingValueOrDefault(&ctx, api.SystemSettingMaxUploadSizeMiBName, "0")
74+
var settingMaxUploadSizeBytes int
75+
if settingMaxUploadSizeMiB, err := strconv.Atoi(maxUploadSetting); err == nil {
76+
settingMaxUploadSizeBytes = settingMaxUploadSizeMiB * MebiByte
77+
} else {
78+
log.Warn("Failed to parse max upload size", zap.Error(err))
79+
settingMaxUploadSizeBytes = 0
7280
}
7381

7482
file, err := c.FormFile("file")
@@ -79,6 +87,14 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
7987
return echo.NewHTTPError(http.StatusBadRequest, "Upload file not found").SetInternal(err)
8088
}
8189

90+
if file.Size > int64(settingMaxUploadSizeBytes) {
91+
message := fmt.Sprintf("File size exceeds allowed limit of %d MiB", settingMaxUploadSizeBytes/MebiByte)
92+
return echo.NewHTTPError(http.StatusBadRequest, message).SetInternal(err)
93+
}
94+
if err := c.Request().ParseMultipartForm(maxUploadBufferSizeBytes); err != nil {
95+
return echo.NewHTTPError(http.StatusBadRequest, "Failed to parse upload data").SetInternal(err)
96+
}
97+
8298
filetype := file.Header.Get("Content-Type")
8399
size := file.Size
84100
sourceFile, err := file.Open()

server/system.go

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
4444
AllowSignUp: false,
4545
IgnoreUpgrade: false,
4646
DisablePublicMemos: false,
47+
MaxUploadSizeMiB: 32,
4748
AdditionalStyle: "",
4849
AdditionalScript: "",
4950
CustomizedProfile: api.CustomizedProfile{
@@ -74,27 +75,40 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
7475
continue
7576
}
7677

77-
if systemSetting.Name == api.SystemSettingAllowSignUpName {
78+
switch systemSetting.Name {
79+
case api.SystemSettingAllowSignUpName:
7880
systemStatus.AllowSignUp = baseValue.(bool)
79-
} else if systemSetting.Name == api.SystemSettingIgnoreUpgradeName {
81+
82+
case api.SystemSettingIgnoreUpgradeName:
8083
systemStatus.IgnoreUpgrade = baseValue.(bool)
81-
} else if systemSetting.Name == api.SystemSettingDisablePublicMemosName {
84+
85+
case api.SystemSettingDisablePublicMemosName:
8286
systemStatus.DisablePublicMemos = baseValue.(bool)
83-
} else if systemSetting.Name == api.SystemSettingAdditionalStyleName {
87+
88+
case api.SystemSettingMaxUploadSizeMiBName:
89+
systemStatus.MaxUploadSizeMiB = int(baseValue.(float64))
90+
91+
case api.SystemSettingAdditionalStyleName:
8492
systemStatus.AdditionalStyle = baseValue.(string)
85-
} else if systemSetting.Name == api.SystemSettingAdditionalScriptName {
93+
94+
case api.SystemSettingAdditionalScriptName:
8695
systemStatus.AdditionalScript = baseValue.(string)
87-
} else if systemSetting.Name == api.SystemSettingCustomizedProfileName {
96+
97+
case api.SystemSettingCustomizedProfileName:
8898
customizedProfile := api.CustomizedProfile{}
89-
err := json.Unmarshal([]byte(systemSetting.Value), &customizedProfile)
90-
if err != nil {
99+
if err := json.Unmarshal([]byte(systemSetting.Value), &customizedProfile); err != nil {
91100
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting customized profile value").SetInternal(err)
92101
}
93102
systemStatus.CustomizedProfile = customizedProfile
94-
} else if systemSetting.Name == api.SystemSettingStorageServiceIDName {
103+
104+
case api.SystemSettingStorageServiceIDName:
95105
systemStatus.StorageServiceID = int(baseValue.(float64))
96-
} else if systemSetting.Name == api.SystemSettingLocalStoragePathName {
106+
107+
case api.SystemSettingLocalStoragePathName:
97108
systemStatus.LocalStoragePath = baseValue.(string)
109+
110+
default:
111+
log.Warn("Unknown system setting name", zap.String("setting name", systemSetting.Name.String()))
98112
}
99113
}
100114

store/system_setting.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@ func (s *Store) FindSystemSetting(ctx context.Context, find *api.SystemSettingFi
9494
return systemSettingRaw.toSystemSetting(), nil
9595
}
9696

97+
func (s *Store) GetSystemSettingValueOrDefault(ctx *context.Context, find api.SystemSettingName, defaultValue string) string {
98+
if setting, err := s.FindSystemSetting(*ctx, &api.SystemSettingFind{
99+
Name: find,
100+
}); err == nil {
101+
return setting.Value
102+
}
103+
return defaultValue
104+
}
105+
97106
func upsertSystemSetting(ctx context.Context, tx *sql.Tx, upsert *api.SystemSettingUpsert) (*systemSettingRaw, error) {
98107
query := `
99108
INSERT INTO system_setting (
@@ -127,7 +136,7 @@ func findSystemSettingList(ctx context.Context, tx *sql.Tx, find *api.SystemSett
127136
query := `
128137
SELECT
129138
name,
130-
value,
139+
value,
131140
description
132141
FROM system_setting
133142
WHERE ` + strings.Join(where, " AND ")

web/src/components/CreateIdentityProviderDialog.tsx

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useEffect, useState } from "react";
22
import { toast } from "react-hot-toast";
3-
import { Button, Divider, Input, Radio, RadioGroup, Typography } from "@mui/joy";
3+
import { Button, Divider, Input, Option, Select, Typography } from "@mui/joy";
44
import * as api from "@/helpers/api";
55
import { UNKNOWN_ID } from "@/helpers/consts";
66
import { absolutifyLink } from "@/helpers/utils";
@@ -101,6 +101,7 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
101101
},
102102
},
103103
];
104+
const identityProviderTypes = [...new Set(templateList.map((t) => t.type))];
104105
const { confirmCallback, destroy, identityProvider } = props;
105106
const [basicInfo, setBasicInfo] = useState({
106107
name: "",
@@ -121,7 +122,7 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
121122
},
122123
});
123124
const [oauth2Scopes, setOAuth2Scopes] = useState<string>("");
124-
const [seletedTemplate, setSelectedTemplate] = useState<string>("GitHub");
125+
const [selectedTemplate, setSelectedTemplate] = useState<string>("GitHub");
125126
const isCreating = identityProvider === undefined;
126127

127128
useEffect(() => {
@@ -143,7 +144,7 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
143144
return;
144145
}
145146

146-
const template = templateList.find((t) => t.name === seletedTemplate);
147+
const template = templateList.find((t) => t.name === selectedTemplate);
147148
if (template) {
148149
setBasicInfo({
149150
name: template.name,
@@ -155,7 +156,7 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
155156
setOAuth2Scopes(template.config.oauth2Config.scopes.join(" "));
156157
}
157158
}
158-
}, [seletedTemplate]);
159+
}, [selectedTemplate]);
159160

160161
const handleCloseBtnClick = () => {
161162
destroy();
@@ -229,37 +230,34 @@ const CreateIdentityProviderDialog: React.FC<Props> = (props: Props) => {
229230
return (
230231
<>
231232
<div className="dialog-header-container">
232-
<p className="title-text">{t("setting.sso-section." + (isCreating ? "create" : "update") + "-sso")}</p>
233-
<button className="btn close-btn" onClick={handleCloseBtnClick}>
233+
<p className="title-text ml-auto">{t("setting.sso-section." + (isCreating ? "create" : "update") + "-sso")}</p>
234+
<button className="btn close-btn ml-auto" onClick={handleCloseBtnClick}>
234235
<Icon.X />
235236
</button>
236237
</div>
237-
<div className="dialog-content-container w-full max-w-[24rem] min-w-[25rem]">
238+
<div className="dialog-content-container min-w-[19rem]">
238239
{isCreating && (
239240
<>
240241
<Typography className="!mb-1" level="body2">
241242
{t("common.type")}
242243
</Typography>
243-
<RadioGroup className="mb-2" value={type}>
244-
<div className="mt-2 w-full flex flex-row space-x-4">
245-
<Radio value="OAUTH2" label="OAuth 2.0" />
246-
</div>
247-
</RadioGroup>
244+
<Select className="w-full mb-4" value={type} onChange={(_, e) => setType(e ?? type)}>
245+
{identityProviderTypes.map((kind) => (
246+
<Option key={kind} value={kind}>
247+
{kind}
248+
</Option>
249+
))}
250+
</Select>
248251
<Typography className="mb-2" level="body2">
249252
{t("setting.sso-section.template")}
250253
</Typography>
251-
<RadioGroup className="mb-2" value={seletedTemplate}>
252-
<div className="mt-2 w-full flex flex-row space-x-4">
253-
{templateList.map((template) => (
254-
<Radio
255-
key={template.name}
256-
value={template.name}
257-
label={template.name}
258-
onChange={(e) => setSelectedTemplate(e.target.value)}
259-
/>
260-
))}
261-
</div>
262-
</RadioGroup>
254+
<Select className="mb-1 h-auto w-full" value={selectedTemplate} onChange={(_, e) => setSelectedTemplate(e ?? selectedTemplate)}>
255+
{templateList.map((template) => (
256+
<Option key={template.name} value={template.name}>
257+
{template.name}
258+
</Option>
259+
))}
260+
</Select>
263261
<Divider className="!my-2" />
264262
</>
265263
)}

0 commit comments

Comments
 (0)