fix: admin/files: changing protocol of base url was not saved
Massimo Melina committed
Jun 10, 2023 at 23:32 UTC
93bdf6cee39dcbae12b6b4b2509150dda930c1cf
4 files changed
+48
-32
admin/src/FileForm.ts
+36
-26
@@ -16,6 +16,7 @@ import {
16
import { apiCall, useApiEx } from './api'
17
import {
18
basename,
19
+ Btn,
20
formatBytes,
21
IconBtn,
22
isEqualLax,
@@ -30,9 +31,9 @@ import { reloadVfs, VfsNode, VfsPerms, Who } from './VfsPage'
31
import md from './md'
32
import _ from 'lodash'
33
import FileField from './FileField'
33
-import { alertDialog, useDialogBarColors } from './dialog'
34
+import { alertDialog, toast, useDialogBarColors } from './dialog'
35
import yaml from 'yaml'
35
-import { Check, ContentCopy, Delete, Edit } from '@mui/icons-material'
36
+import { Check, ContentCopy, Delete, Edit, Save } from '@mui/icons-material'
37
38
interface Account { username: string }
39
@@ -41,12 +42,12 @@ interface FileFormProps {
42
anyMask?: boolean
43
defaultPerms: VfsPerms
44
addToBar?: ReactNode
44
- urls: string[] | false
45
+ statusApi: any
46
}
47
48
const ACCEPT_LINK = "https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept"
49
49
-export default function FileForm({ file, anyMask, defaultPerms, addToBar, urls }: FileFormProps) {
50
+export default function FileForm({ file, anyMask, defaultPerms, addToBar, statusApi }: FileFormProps) {
51
const { parent, children, isRoot, byMasks, ...rest } = file
52
const [values, setValues] = useState(rest)
53
useEffect(() => {
@@ -111,7 +112,7 @@ export default function FileForm({ file, anyMask, defaultPerms, addToBar, urls }
112
fields: [
113
isRoot ? h(Alert,{ severity: 'info' }, "This is Home, the root of your shared files. Options set here will be applied to all files.")
114
: { k: 'name', required: true, helperText: hasSource && "You can decide a name that's different from the one on your disk" },
114
- { k: 'id', comp: LinkField, urls },
115
+ { k: 'id', comp: LinkField, statusApi },
116
{ k: 'source', label: "Source on disk", comp: FileField, files: !isDir, folders: isDir, multiline: true,
117
placeholder: "Not on disk, this is a virtual folder",
118
},
@@ -219,15 +220,18 @@ function who2desc(who: any) {
220
}
221
222
interface LinkFieldProps extends FieldProps<string> {
222
- urls: string[]
223
+ statusApi: any // receive status from parent, to avoid asking server at each click on a file
224
}
224
-function LinkField({ value, urls }: LinkFieldProps) {
225
- const { data, error, reload } = useApiEx('get_config', { only: ['base_url'] })
226
- const base: string | undefined = data?.base_url
227
- const link = (base || (urls ? urls[0] : '')) + value
228
- return h(Box, { display: 'flex', },
225
+function LinkField({ value, statusApi }: LinkFieldProps) {
226
+ const { data: status, reload, error } = statusApi
227
+ const urls: string[] = status?.suggestedUrls
228
+ const base: string = status?.baseUrl
229
+ const link = (base || urls?.[0] || '') + value
230
+ return h(Box, { display: 'flex' },
231
+ !urls ? 'error' : // check data is ok
232
h(DisplayField, {
230
- label: "Link", value: link,
233
+ label: "Link",
234
+ value: link,
235
error,
236
end: h(Box, {},
237
h(IconBtn, {
@@ -235,23 +239,18 @@ function LinkField({ value, urls }: LinkFieldProps) {
239
title: "Copy",
240
onClick: () => navigator.clipboard.writeText(link)
241
}),
238
- h(IconBtn, {
239
- icon: Edit,
240
- title: "Change",
241
- onClick: edit,
242
- }),
242
+ h(IconBtn, { icon: Edit, title: "Change", onClick: edit }),
243
)
244
}),
245
)
246
247
function edit() {
248
- const startingProto = new URL(base || urls[0]).protocol + '//'
249
- newDialog({
248
+ const close = newDialog({
249
title: "Change link",
251
- onClose: reload,
250
Content() {
253
- const [v, setV] = useState(base)
254
- const [proto, setProto] = useState(startingProto)
251
+ const [v, setV] = useState(base || '')
252
+ const proto = new URL(v || urls[0]).protocol + '//'
253
+ const host = urls.includes(v) ? '' : v.slice(proto.length)
254
return h(Box, { display: 'flex', flexDirection: 'column' },
255
h(Box, { mb: 2 }, "You can choose a different base address for your links"),
256
h(MenuList, {},
@@ -264,11 +263,11 @@ function LinkField({ value, urls }: LinkFieldProps) {
263
h(StringField, {
264
label: "Custom IP or domain",
265
helperText: md("You can type any address but *you* are responsible to make the address work.\nThis functionality is just to help you copy the link in case you have a domain or a complex network configuration."),
267
- value: !v || urls.includes(v) ? '' : v.slice(proto.length),
266
+ value: host,
267
onChange: v => set(prefix(proto, v)),
268
start: h(SelectField as Field<string>, {
269
value: proto,
271
- onChange: setProto,
270
+ onChange: v => host ? set(v + host) : toast("Enter domain first"),
271
options: ['http://','https://'],
272
size: 'small',
273
variant: 'standard',
@@ -276,12 +275,23 @@ function LinkField({ value, urls }: LinkFieldProps) {
275
}),
276
sx: { mt: 2 }
277
}),
278
+ h(Box, { mt: 2, textAlign: 'right' },
279
+ h(Btn, {
280
+ icon: Save,
281
+ children: "Save",
282
+ async onClick() {
283
+ if (v !== base) {
284
+ await apiCall('set_config', { values: { base_url: v } })
285
+ await reload()
286
+ }
287
+ close()
288
+ },
289
+ }) ),
290
)
291
281
- async function set(u: string) {
292
+ function set(u: string) {
293
if (u.endsWith('/'))
294
u = u.slice(0, -1)
284
- await apiCall('set_config', { values: { base_url: u } })
295
setV(u)
296
}
297
}
admin/src/VfsPage.ts
+9
-4
@@ -38,9 +38,14 @@ export default function VfsPage() {
38
[vfs])
39
const sideBreakpoint = 'md'
40
const isSideBreakpoint = useBreakpoint(sideBreakpoint)
41
- const [status] = useApi('get_status')
42
- const urls = useMemo(() => _.sortBy(status?.urls.https || status?.urls.http, url => url.includes('[')), // ipv4 first
43
- [status])
41
+ const statusApi = useApiEx('get_status')
42
+ const { data: status } = statusApi
43
+ const urls = useMemo(() => {
44
+ const b = status?.baseUrl
45
+ const ret = _.sortBy(status?.urls.https || status?.urls.http, url => url.includes('[') && url !== b) // ipv4 first
46
+ if (status) status.suggestedUrls = ret // store it for Link component
47
+ return b && !ret.includes(b) ? [b, ...ret] : ret
48
+ }, [status])
49
50
function close() {
51
state.selectedFiles = []
@@ -55,7 +60,7 @@ export default function VfsPage() {
60
}),
61
defaultPerms: data?.defaultPerms as VfsPerms,
62
anyMask,
58
- urls,
63
+ statusApi,
64
file: selectedFiles[0] as VfsNode // it's actually Snapshot<VfsNode> but it's easier this way
65
})
66
: h(Fragment, {},
src/adminApis.ts
+2
-1
@@ -22,7 +22,7 @@ import { debounceAsync, isLocalHost, makeNetMatcher, onOff, waitFor } from './mi
22
import events from './events'
23
import { accountCanLoginAdmin, accountsConfig, getFromAccount } from './perm'
24
import Koa from 'koa'
25
-import { getProxyDetected } from './middlewares'
25
+import { baseUrl, getProxyDetected } from './middlewares'
26
import { writeFile } from 'fs/promises'
27
import { createReadStream } from 'fs'
28
import * as readline from 'readline'
@@ -89,6 +89,7 @@ export const adminApis: ApiHandlers = {
89
compatibleApiVersion: COMPATIBLE_API_VERSION,
90
...await getServerStatus(),
91
urls: getUrls(),
92
+ baseUrl: baseUrl.get(), // can be retrieved with get_config, but it's very handy with urls and low overhead. Case is different because the context is
93
update: !updateSupported() ? false : await localUpdateAvailable() ? 'local' : true,
94
proxyDetected: getProxyDetected(),
95
frpDetected: localhostAdmin.get() && !getProxyDetected()
src/middlewares.ts
+1
-1
@@ -170,7 +170,7 @@ const errorMessages = {
170
[HTTP_FORBIDDEN]: "Forbidden",
171
}
172
173
-const baseUrl = defineConfig('base_url', '')
173
+export const baseUrl = defineConfig('base_url', '')
174
175
async function sendFolderList(node: VfsNode, ctx: Koa.Context) {
176
let { depth=0, folders, prepend } = ctx.query