fix: (regression 0.57.16) admin/shared: adding a folder from disk didn't work correctly until restart #1082

Massimo Melina committed Sep 15, 2025 at 19:42 UTC e638266c905c14823ec2a0a107a980f8f5619a04
5 files changed +30 -13
admin/src/AccountForm.ts
+1 -1
@@ -6,7 +6,7 @@ import { Alert, Box } from '@mui/material'
6 import { apiCall } from './api'
7 import { alertDialog, useDialogBarColors } from './dialog'
8 import { formatTimestamp, isEqualLax, prefix, reactJoin, useIsMobile, wantArray } from './misc'
9 -import { Btn, Flex, IconBtn, NetmaskField, propsForModifiedValues, useLogBreakpoint } from './mui'
9 +import { Btn, Flex, IconBtn, NetmaskField, propsForModifiedValues } from './mui'
10 import { Account } from './AccountsPage'
11 import { createVerifierAndSalt, SRPParameters, SRPRoutines } from 'tssrp6a'
12 import { AutoDelete, Delete } from '@mui/icons-material'
src/api.vfs.ts
+1 -2
@@ -165,9 +165,8 @@ const apis: ApiHandlers = {
165 return HTTP_SERVER_ERROR
166 const idx = children.indexOf(node)
167 children.splice(idx, 1)
168 - await saveVfs()
168 return 0 // error code 0 is OK
170 - }))
169 + })).finally(saveVfs)
170 }
171 },
172
src/config.ts
+2 -2
@@ -128,7 +128,7 @@ export async function setConfig(newCfg: Record<string,unknown>, save?: boolean)
128 const argCfg = !started && _.pickBy(newObj(configProps,
129 (x, k) => argv[k] ?? tryJson(considerEnvs ? process.env['HFS_' + k.toUpperCase().replaceAll('-','_')] : '', _.identity)),
130 x => x !== undefined)
131 - if (! _.isEmpty(argCfg)) {
131 + if (!_.isEmpty(argCfg)) {
132 saveConfigAsap() // don't set `save` argument, as it would interfere below at check `save===false`
133 Object.assign(newCfg, argCfg)
134 }
@@ -149,7 +149,7 @@ export async function setConfig(newCfg: Record<string,unknown>, save?: boolean)
149 newCfg.hasOwnProperty(k) || apply(k, undefined, true)))
150 started = true
151 events.emit('configReady', startedWithoutConfig)
152 - if (version?.valueOf() !== VERSION) // be sure to save version
152 + if (version?.valueOf() !== VERSION) // be sure to save the new version in the file
153 saveConfigAsap()
154
155 function apply(k: string, newV: any, isDefault=false) {
src/vfs.ts
+9 -6
@@ -3,7 +3,7 @@
3 import fs from 'fs/promises'
4 import { basename, dirname, join, resolve } from 'path'
5 import {
6 - makeMatcher, setHidden, onlyTruthy, isValidFileName, throw_, VfsPerms, Who,
6 + makeMatcher, setHidden, onlyTruthy, isValidFileName, throw_, VfsPerms, Who, debounceAsync,
7 isWhoObject, WHO_ANY_ACCOUNT, defaultPerms, PERM_KEYS, removeStarting, HTTP_SERVER_ERROR, try_, matches,
8 } from './misc'
9 import Koa from 'koa'
@@ -171,7 +171,9 @@ export async function getNodeByName(name: string, parent: VfsNode) {
171 }
172
173 export let vfs: VfsNode = {}
174 -defineConfig('vfs', vfs).sub(async data => {
174 +defineConfig('vfs', vfs).sub(reviewVfs)
175 +
176 +async function reviewVfs(data=vfs) {
177 await (async function recur(node) {
178 if (node.source && !node.children?.length && node.isFolder === undefined) {
179 const isFolder = /[\\/]$/.test(node.source) || (await nodeStats(node))?.isDirectory()
@@ -181,12 +183,13 @@ defineConfig('vfs', vfs).sub(async data => {
183 await Promise.allSettled(node.children.map(recur))
184 })(data)
185 vfs = data
184 -})
185 -
186 -export function saveVfs() {
187 - return setConfig({ vfs: _.cloneDeep(vfs) }, true)
186 }
187
188 +export const saveVfs = debounceAsync(async () => {
189 + await reviewVfs()
190 + await setConfig({ vfs }, true)
191 +})
192 +
193 export function isRoot(node: VfsNode) {
194 return node === vfs
195 }
tests/test.ts
+17 -2
@@ -269,6 +269,20 @@ describe('after-login', () => {
269 after(() => rm(resolve(__dirname, 'temp'), { recursive: true }).catch(() => 0))
270 })
271
272 +describe('admin', () => {
273 + const auth = `${username}:${password}`
274 + test('add folder', async () => {
275 + const name = 'added'
276 + try {
277 + await reqApi('add_vfs', { source: '.', name, can_see: { this: false, children: true } }, 200, { auth })() // add an invisible folder
278 + await reqList(name, { inList: ['plugins/'] })()
279 + }
280 + finally {
281 + await reqApi('del_vfs', { uris: ['/'+name] }, 200, { auth })() // remove
282 + }
283 + })
284 +})
285 +
286 function login(usr: string, pwd=password) {
287 return srpClientSequence(usr, pwd, (cmd: string, params: any) =>
288 reqApi(cmd, params, (x,res)=> res.statusCode < 400)())
@@ -390,11 +404,12 @@ function req(url: string, test:Tester, { baseUrl, throttle, ...requestOptions }:
404 }
405 }
406
393 -function reqApi(api: string, params: object, test:Tester) {
407 +function reqApi(api: string, params: object, test:Tester, options:any={}) {
408 const isGet = api.startsWith('/')
409 return req(API+api, test, {
410 body: JSON.stringify(params),
397 - headers: isGet ? undefined : { 'x-hfs-anti-csrf': '1'}
411 + headers: isGet ? undefined : { 'x-hfs-anti-csrf': '1'},
412 + ...options,
413 })
414 }
415