admin/fs: system integration (Windows only) #347

Massimo Melina committed Oct 17, 2023 at 10:58 UTC 0d8f2efd6a96ea1b6f3c53055e74a14823962d13
4 files changed +54 -7
admin/src/VfsMenuBar.ts
+26 -3
@@ -2,13 +2,15 @@
2
3 import { createElement as h } from 'react'
4 import { Box } from '@mui/material'
5 -import { Add } from '@mui/icons-material'
5 +import { Add, Microsoft } from '@mui/icons-material'
6 import { reloadVfs } from './VfsPage'
7 import addFiles, { addVirtual } from './addFiles'
8 import MenuButton from './MenuButton'
9 -import { reloadBtn } from './misc'
9 +import { basename, Btn, reloadBtn } from './misc'
10 +import { apiCall } from './api'
11 +import { alertDialog, confirmDialog } from './dialog'
12
11 -export default function VfsMenuBar() {
13 +export default function VfsMenuBar({ status }: any) {
14 return h(Box, {
15 display: 'flex',
16 gap: 2,
@@ -30,5 +32,26 @@ export default function VfsMenuBar() {
32 ]
33 }, "Add"),
34 reloadBtn(() => reloadVfs()),
35 + status?.platform === 'win32' && h(Btn, {
36 + icon: Microsoft,
37 + variant: 'outlined',
38 + onClick: windowsIntegration
39 + }, "System integration"),
40 )
41 }
42 +
43 +async function windowsIntegration() {
44 + const msg = h(Box, {}, "We are going to add a command in the right-click of Windows File Manager",
45 + h('img', { src: 'win-shell.png', style: {
46 + display: 'block',
47 + width: 'min(30em, 80vw)',
48 + marginTop: '1em',
49 + } }),
50 + )
51 + if (!await confirmDialog(msg)) return
52 + 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 })
54 + hint.close()
55 + return finish ? alertDialog("To finish the process, please execute the file you'll find on your desktop: " + basename(finish))
56 + : alertDialog("Done!", 'success')
57 +}
\ No newline at end of file
admin/src/VfsPage.ts
+1 -1
@@ -119,7 +119,7 @@ export default function VfsPage() {
119 h(Grid, { item:true, [sideBreakpoint]: 6, lg: 5, xl: 4 },
120 h(Typography, { variant: 'h6', mb:1, }, "Virtual File System"),
121 h(Alert, { severity: 'info' }, "If you rename or delete here, it's virtual, and only affects what is presented to the users"),
122 - h(VfsMenuBar),
122 + h(VfsMenuBar, { status }),
123 vfs && h(VfsTree, { id2node })),
124 isSideBreakpoint && sideContent && h(Grid, { item:true, [sideBreakpoint]: true, maxWidth:'100%' },
125 h(Card, { sx: { overflow: 'initial' } }, // overflow is incompatible with stickyBar
src/adminApis.ts
+1
@@ -102,6 +102,7 @@ export const adminApis: ApiHandlers = {
102 apiVersion: API_VERSION,
103 compatibleApiVersion: COMPATIBLE_API_VERSION,
104 ...await getServerStatus(),
105 + platform: process.platform,
106 urls: await getUrls(),
107 ips: await getIps(false),
108 baseUrl: getBaseUrlOrDefault(),
src/api.vfs.ts
+26 -3
@@ -3,7 +3,7 @@
3 import { getNodeName, isSameFilenameAs, nodeIsDirectory, saveVfs, urlToNode, vfs, VfsNode, applyParentToChild,
4 permsFromParent } from './vfs'
5 import _ from 'lodash'
6 -import { stat } from 'fs/promises'
6 +import { stat, writeFile } from 'fs/promises'
7 import { ApiError, ApiHandlers, SendListReadable } from './apiMiddleware'
8 import { dirname, extname, join, resolve } from 'path'
9 import { dirStream, enforceFinal, isDirectory, isWindowsDrive, makeMatcher, PERM_KEYS, VfsNodeAdminSend } from './misc'
@@ -13,7 +13,9 @@ import {
13 } from './const'
14 import { getDrives } from './util-os'
15 import { Stats } from 'fs'
16 -import { getBaseUrlOrDefault } from './listen'
16 +import { getBaseUrlOrDefault, getServerStatus } from './listen'
17 +import { homedir } from 'os'
18 +import open from 'open'
19
20 // to manipulate the tree we need the original node
21 async function urlToNodeOriginal(uri: string) {
@@ -204,7 +206,11 @@ const apis: ApiHandlers = {
206 }
207 }
208 })
207 - }
209 + },
210 +
211 + async windows_integration() {
212 + return { finish: await windowsIntegration() }
213 + },
214
215 }
216
@@ -224,4 +230,21 @@ function simplifyName(node: VfsNode) {
230 const { name, ...noName } = node
231 if (getNodeName(noName) === name)
232 delete node.name
233 +}
234 +
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)"
242 +
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 + `)
246 + const path = homedir() + '\\desktop\\hfs-windows-menu.reg'
247 + await writeFile(path, content, 'utf8')
248 + try { await open(path, { wait: true}) }
249 + catch { return path }
250 }
\ No newline at end of file