admin/fs: offer removal after system integration

Massimo Melina committed Oct 22, 2023 at 09:34 UTC 2f3b33b1b3efb29266cc1d2ee14514d8137776d0
5 files changed +54 -22
admin/public/win-shell.png
Binary files /dev/null and b/admin/public/win-shell.png differ
admin/src/VfsMenuBar.ts
+18 -6
@@ -7,10 +7,11 @@ import { reloadVfs } from './VfsPage'
7 import addFiles, { addVirtual } from './addFiles'
8 import MenuButton from './MenuButton'
9 import { basename, Btn, reloadBtn } from './misc'
10 -import { apiCall } from './api'
10 +import { apiCall, useApi } from './api'
11 import { alertDialog, confirmDialog } from './dialog'
12
13 export default function VfsMenuBar({ status }: any) {
14 + const { data: integrated, reload } = useApi(status?.platform === 'win32' && 'windows_integrated')
15 return h(Box, {
16 display: 'flex',
17 gap: 2,
@@ -35,8 +36,15 @@ export default function VfsMenuBar({ status }: any) {
36 status?.platform === 'win32' && h(Btn, {
37 icon: Microsoft,
38 variant: 'outlined',
38 - onClick: windowsIntegration
39 - }, "System integration"),
39 + ...(!integrated?.is ? {
40 + children: "System integration",
41 + onClick: () => windowsIntegration().then(reload),
42 + } : {
43 + confirm: true,
44 + children: "Remove integration",
45 + onClick: () => exec('windows_remove').then(reload),
46 + })
47 + }),
48 )
49 }
50
@@ -48,10 +56,14 @@ async function windowsIntegration() {
56 marginTop: '1em',
57 } }),
58 )
51 - if (!await confirmDialog(msg)) return
59 + return await confirmDialog(msg)
60 + && exec('windows_integration')
61 +}
62 +
63 +async function exec(api: string) {
64 const hint = alertDialog("Click YES to the next 2 dialogs. The second dialog may not appear, and you need to click on the bottom bar.", 'warning')
53 - const { finish } = await apiCall('windows_integration', {}, { timeout: 60 })
65 + const { finish } = await apiCall(api, {}, { timeout: false })
66 hint.close()
67 return finish ? alertDialog("To finish the process, please execute the file you'll find on your desktop: " + basename(finish))
68 : alertDialog("Done!", 'success')
57 -}
\ No newline at end of file
69 +}
admin/src/addFiles.ts
+5 -3
@@ -23,8 +23,9 @@ export default function addFiles() {
23 h(FilePicker, {
24 from: parent.source,
25 async onSelect(sel) {
26 - const errs = onlyTruthy(await Promise.all(sel.map(source =>
27 - apiCall('add_vfs', { parent: parent.id, source }).then(() => null, e => [source, e.message]))))
26 + const res = await Promise.all(sel.map(source =>
27 + apiCall('add_vfs', { parent: parent.id, source }).then(r => r, e => [source, e.message])))
28 + const errs = res.filter(Array.isArray)
29 if (errs.length)
30 await alertDialog(h(Box, {},
31 "Some elements have been rejected",
@@ -33,7 +34,8 @@ export default function addFiles() {
34 h('li', { key: file }, file, ': ', err))
35 )
36 ), 'error')
36 - reloadVfs()
37 + const good = res.filter(x => x.name).map(x => parent.id + x.name)
38 + reloadVfs(good)
39 close()
40 }
41 })
admin/src/mui.ts
+2 -2
@@ -134,7 +134,7 @@ interface BtnProps extends Omit<LoadingButtonProps,'disabled'|'title'|'onClick'>
134 disabled?: boolean | string
135 progress?: boolean | number
136 link?: string
137 - confirm?: string
137 + confirm?: boolean | string
138 tooltipProps?: TooltipProps
139 onClick: (...args: Parameters<NonNullable<ButtonProps['onClick']>>) => Promisable<any>
140 }
@@ -156,7 +156,7 @@ export function Btn({ icon, title, onClick, disabled, progress, link, tooltipPro
156 disabled,
157 ...rest,
158 async onClick(...args) {
159 - if (confirm && !await confirmDialog(confirm)) return
159 + if (confirm && !await confirmDialog(confirm === true ? "Are you sure?" : confirm)) return
160 const ret = onClick?.apply(this,args)
161 if (ret && ret instanceof Promise) {
162 setLoading(true)
src/api.vfs.ts
+29 -11
@@ -16,6 +16,8 @@ import { Stats } from 'fs'
16 import { getBaseUrlOrDefault, getServerStatus } from './listen'
17 import { homedir } from 'os'
18 import open from 'open'
19 +import { promisify } from 'util'
20 +import { execFile } from 'child_process'
21
22 // to manipulate the tree we need the original node
23 async function urlToNodeOriginal(uri: string) {
@@ -212,6 +214,17 @@ const apis: ApiHandlers = {
214 return { finish: await windowsIntegration() }
215 },
216
217 + async windows_integrated() {
218 + return {
219 + is: await promisify(execFile)('reg', ['query', WINDOWS_REG_KEY])
220 + .then(x => x.stdout.includes('REG_SZ'), () => false)
221 + }
222 + },
223 +
224 + async windows_remove() { // not using `reg delete` because it doesn't ask for admin permissions
225 + return { finish: await runReg(['*', 'Directory'].map(k => `\n\n[-${WINDOWS_REG_KEY.replace('*', k)}]`).join('')) }
226 + },
227 +
228 }
229
230 export default apis
@@ -232,22 +245,27 @@ function simplifyName(node: VfsNode) {
245 delete node.name
246 }
247
235 -async function windowsIntegration() {
236 - const status = await getServerStatus()
237 - const url = 'http://localhost:' + status.http.port
238 - const content = `Windows Registry Editor Version 5.00
239 - `+ ['*', 'Directory'].map(k => `
240 -[HKEY_CLASSES_ROOT\\${k}\\shell\\AddToHFS3]
241 -@="Add to HFS (new)"
248 +const WINDOWS_REG_KEY = 'HKEY_CLASSES_ROOT\\*\\shell\\AddToHFS3'
249 +const WINDOWS_REG_KEY2 = WINDOWS_REG_KEY + '\\command'
250
243 -[HKEY_CLASSES_ROOT\\${k}\\shell\\AddToHFS3\\command]
244 -@="powershell -Command \\"$p = '%1'.Replace('\\\\', '\\\\\\\\'); $j = '{ \\\\\\"source\\\\\\": \\\\\\"' + $p + '\\\\\\" }'; $wsh = New-Object -ComObject Wscript.Shell; try { $res = Invoke-WebRequest -Uri '${url}/~/api/add_vfs' -Method POST -Headers @{ 'x-hfs-anti-csrf' = '1' } -ContentType 'application/json' -TimeoutSec 1 -Body $j; $json = $res.Content | ConvertFrom-Json; $link = $json.link; $link | Set-Clipboard; } catch { $wsh.Popup('Server is down', 0, 'Error', 16); }\\""
245 - `)
251 +export async function runReg(content: string) {
252 const path = homedir() + '\\desktop\\hfs-windows-menu.reg'
247 - await writeFile(path, content, 'utf8')
253 + await writeFile(path, 'Windows Registry Editor Version 5.00\n\n' + content, 'utf8')
254 try {
255 await open(path, { wait: true})
256 await unlink(path)
257 }
258 catch { return path }
259 +}
260 +
261 +export async function windowsIntegration() {
262 + const status = await getServerStatus()
263 + const url = 'http://localhost:' + status.http.port
264 + return runReg(['*', 'Directory'].map(k => `
265 +[${WINDOWS_REG_KEY.replace('*', k)}]
266 +@="Add to HFS (new)"
267 +
268 +[${WINDOWS_REG_KEY2.replace('*', k)}]
269 +@="powershell -Command \\"$p = '%1'.Replace('\\\\', '\\\\\\\\'); $j = '{ \\\\\\"source\\\\\\": \\\\\\"' + $p + '\\\\\\" }'; $wsh = New-Object -ComObject Wscript.Shell; try { $res = Invoke-WebRequest -Uri '${url}/~/api/add_vfs' -Method POST -Headers @{ 'x-hfs-anti-csrf' = '1' } -ContentType 'application/json' -TimeoutSec 1 -Body $j; $json = $res.Content | ConvertFrom-Json; $link = $json.link; $link | Set-Clipboard; } catch { $wsh.Popup('Server is down', 0, 'Error', 16); }\\""
270 + `).join(''))
271 }
\ No newline at end of file