svbridge 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require('../server.js')
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "svbridge",
3
+ "version": "1.0.0",
4
+ "description": "StorageVisual local file bridge — gives the web app full disk access",
5
+ "main": "server.js",
6
+ "bin": {
7
+ "svbridge": "svbridge"
8
+ },
9
+ "scripts": {
10
+ "start": "node server.js"
11
+ },
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "keywords": ["storagevisual", "bridge", "disk", "storage"],
16
+ "license": "MIT"
17
+ }
package/server.js ADDED
@@ -0,0 +1,265 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * StorageVisual Bridge — local file system server
4
+ * Runs at http://localhost:57321
5
+ * Gives the StorageVisual web app full file system access without browser restrictions.
6
+ *
7
+ * Security: CORS restricted to storagevisual.com only.
8
+ * The browser enforces this — no other website can call this server.
9
+ */
10
+
11
+ const http = require('http')
12
+ const fs = require('fs')
13
+ const fsp = require('fs/promises')
14
+ const path = require('path')
15
+ const { exec } = require('child_process')
16
+ const os = require('os')
17
+
18
+ const PORT = 57321
19
+ const VERSION = '1.0.0'
20
+ const ALLOWED_ORIGINS = [
21
+ 'https://storagevisual.com',
22
+ 'https://www.storagevisual.com',
23
+ 'http://localhost:5173',
24
+ 'http://localhost:4173',
25
+ ]
26
+
27
+ // ── CORS headers ─────────────────────────────────────────────────────────────
28
+ function corsHeaders(origin) {
29
+ const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : ALLOWED_ORIGINS[0]
30
+ return {
31
+ 'Access-Control-Allow-Origin': allowed,
32
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
33
+ 'Access-Control-Allow-Headers': 'Content-Type',
34
+ 'Access-Control-Max-Age': '86400',
35
+ }
36
+ }
37
+
38
+ // ── JSON response helper ──────────────────────────────────────────────────────
39
+ function send(res, origin, status, data) {
40
+ const body = JSON.stringify(data)
41
+ res.writeHead(status, {
42
+ 'Content-Type': 'application/json',
43
+ 'Content-Length': Buffer.byteLength(body),
44
+ ...corsHeaders(origin),
45
+ })
46
+ res.end(body)
47
+ }
48
+
49
+ // ── Parse JSON body ───────────────────────────────────────────────────────────
50
+ function readBody(req) {
51
+ return new Promise((resolve, reject) => {
52
+ let data = ''
53
+ req.on('data', chunk => { data += chunk })
54
+ req.on('end', () => {
55
+ try { resolve(data ? JSON.parse(data) : {}) }
56
+ catch (e) { reject(e) }
57
+ })
58
+ req.on('error', reject)
59
+ })
60
+ }
61
+
62
+ // ── Recursive directory scan ──────────────────────────────────────────────────
63
+ async function scanDir(dirPath, rootName, results, limit = 100000) {
64
+ if (results.length >= limit) return
65
+ let entries
66
+ try { entries = await fsp.readdir(dirPath, { withFileTypes: true }) }
67
+ catch { return } // permission denied — skip silently
68
+
69
+ await Promise.all(entries.map(async (entry) => {
70
+ if (results.length >= limit) return
71
+ const fullPath = path.join(dirPath, entry.name)
72
+ if (entry.isDirectory()) {
73
+ await scanDir(fullPath, rootName, results, limit)
74
+ } else if (entry.isFile()) {
75
+ try {
76
+ const stat = await fsp.stat(fullPath)
77
+ // Store path relative to root folder name (matches web app format)
78
+ const rel = path.relative(path.dirname(dirPath), fullPath)
79
+ results.push({
80
+ name: entry.name,
81
+ path: rel.split(path.sep).join('/'),
82
+ size_bytes: stat.size,
83
+ lastModified: stat.mtimeMs,
84
+ })
85
+ } catch { /* skip */ }
86
+ }
87
+ }))
88
+ }
89
+
90
+ // ── Native folder picker ──────────────────────────────────────────────────────
91
+ function pickFolder() {
92
+ return new Promise((resolve, reject) => {
93
+ if (process.platform === 'darwin') {
94
+ exec(`osascript -e 'POSIX path of (choose folder)'`, (err, stdout) => {
95
+ if (err) reject(err)
96
+ else resolve(stdout.trim())
97
+ })
98
+ } else if (process.platform === 'win32') {
99
+ const ps = `Add-Type -AssemblyName System.Windows.Forms; $f=New-Object System.Windows.Forms.FolderBrowserDialog; if($f.ShowDialog() -eq 'OK'){$f.SelectedPath}`
100
+ exec(`powershell -Command "${ps}"`, (err, stdout) => {
101
+ if (err) reject(err)
102
+ else resolve(stdout.trim())
103
+ })
104
+ } else {
105
+ exec(`zenity --file-selection --directory 2>/dev/null || kdialog --getexistingdirectory 2>/dev/null`, (err, stdout) => {
106
+ if (err) reject(err)
107
+ else resolve(stdout.trim())
108
+ })
109
+ }
110
+ })
111
+ }
112
+
113
+ // ── Open file in native app ───────────────────────────────────────────────────
114
+ function openNative(filePath) {
115
+ return new Promise((resolve, reject) => {
116
+ const cmd = process.platform === 'win32'
117
+ ? `start "" "${filePath}"`
118
+ : process.platform === 'darwin'
119
+ ? `open "${filePath}"`
120
+ : `xdg-open "${filePath}"`
121
+ exec(cmd, (err) => err ? reject(err) : resolve())
122
+ })
123
+ }
124
+
125
+ // ── Auto-start on login ───────────────────────────────────────────────────────
126
+ function installAutoStart() {
127
+ const exe = process.execPath
128
+ if (process.platform === 'darwin') {
129
+ const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.storagevisual.bridge.plist')
130
+ if (!fs.existsSync(plistPath)) {
131
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
132
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
133
+ <plist version="1.0">
134
+ <dict>
135
+ <key>Label</key><string>com.storagevisual.bridge</string>
136
+ <key>ProgramArguments</key><array><string>${exe}</string></array>
137
+ <key>RunAtLoad</key><true/>
138
+ <key>KeepAlive</key><true/>
139
+ <key>StandardOutPath</key><string>${os.homedir()}/.storagevisual/bridge.log</string>
140
+ <key>StandardErrorPath</key><string>${os.homedir()}/.storagevisual/bridge.log</string>
141
+ </dict>
142
+ </plist>`
143
+ try {
144
+ fs.mkdirSync(path.dirname(plistPath), { recursive: true })
145
+ fs.writeFileSync(plistPath, plist)
146
+ exec(`launchctl load "${plistPath}"`)
147
+ console.log('✅ Auto-start installed (LaunchAgent)')
148
+ } catch (e) {
149
+ console.log('⚠️ Could not install auto-start:', e.message)
150
+ }
151
+ }
152
+ } else if (process.platform === 'win32') {
153
+ const { execSync } = require('child_process')
154
+ try {
155
+ execSync(`reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" /v StorageVisualBridge /t REG_SZ /d "${exe}" /f`)
156
+ console.log('✅ Auto-start installed (Registry)')
157
+ } catch (e) {
158
+ console.log('⚠️ Could not install auto-start:', e.message)
159
+ }
160
+ }
161
+ }
162
+
163
+ // ── Request router ────────────────────────────────────────────────────────────
164
+ const server = http.createServer(async (req, res) => {
165
+ const origin = req.headers.origin || ''
166
+ const url = new URL(req.url, `http://localhost:${PORT}`)
167
+
168
+ // Preflight
169
+ if (req.method === 'OPTIONS') {
170
+ res.writeHead(204, corsHeaders(origin))
171
+ res.end()
172
+ return
173
+ }
174
+
175
+ // Only allow storagevisual.com origins (browser enforces CORS, this is belt-and-suspenders)
176
+ if (origin && !ALLOWED_ORIGINS.includes(origin)) {
177
+ send(res, origin, 403, { error: 'Forbidden' })
178
+ return
179
+ }
180
+
181
+ try {
182
+ // GET /ping — health check
183
+ if (req.method === 'GET' && url.pathname === '/ping') {
184
+ send(res, origin, 200, { ok: true, version: VERSION, platform: process.platform })
185
+ return
186
+ }
187
+
188
+ // GET /pick — open native folder picker, returns { path }
189
+ if (req.method === 'GET' && url.pathname === '/pick') {
190
+ const folderPath = await pickFolder()
191
+ if (!folderPath) { send(res, origin, 204, { path: null }); return }
192
+ send(res, origin, 200, { path: folderPath })
193
+ return
194
+ }
195
+
196
+ // GET /scan?path=/Users/john/Downloads
197
+ if (req.method === 'GET' && url.pathname === '/scan') {
198
+ const scanPath = url.searchParams.get('path')
199
+ if (!scanPath) { send(res, origin, 400, { error: 'Missing path' }); return }
200
+ const resolved = path.resolve(scanPath)
201
+ const files = []
202
+ await scanDir(resolved, path.basename(resolved), files)
203
+ send(res, origin, 200, { files })
204
+ return
205
+ }
206
+
207
+ // POST /open — open file in native app
208
+ if (req.method === 'POST' && url.pathname === '/open') {
209
+ const { path: filePath } = await readBody(req)
210
+ if (!filePath) { send(res, origin, 400, { error: 'Missing path' }); return }
211
+ await openNative(path.resolve(filePath))
212
+ send(res, origin, 200, { ok: true })
213
+ return
214
+ }
215
+
216
+ // POST /delete — delete a file
217
+ if (req.method === 'POST' && url.pathname === '/delete') {
218
+ const { path: filePath } = await readBody(req)
219
+ if (!filePath) { send(res, origin, 400, { error: 'Missing path' }); return }
220
+ await fsp.unlink(path.resolve(filePath))
221
+ send(res, origin, 200, { ok: true })
222
+ return
223
+ }
224
+
225
+ // POST /move — move a file to a new directory
226
+ if (req.method === 'POST' && url.pathname === '/move') {
227
+ const { from, toDir } = await readBody(req)
228
+ if (!from || !toDir) { send(res, origin, 400, { error: 'Missing from or toDir' }); return }
229
+ const src = path.resolve(from)
230
+ const dest = path.join(path.resolve(toDir), path.basename(src))
231
+ await fsp.rename(src, dest).catch(async () => {
232
+ // rename fails across volumes — copy + delete
233
+ await fsp.copyFile(src, dest)
234
+ await fsp.unlink(src)
235
+ })
236
+ send(res, origin, 200, { ok: true, newPath: dest })
237
+ return
238
+ }
239
+
240
+ send(res, origin, 404, { error: 'Not found' })
241
+ } catch (err) {
242
+ send(res, origin, 500, { error: err.message })
243
+ }
244
+ })
245
+
246
+ // ── Start ─────────────────────────────────────────────────────────────────────
247
+ fs.mkdirSync(path.join(os.homedir(), '.storagevisual'), { recursive: true })
248
+
249
+ server.listen(PORT, '127.0.0.1', () => {
250
+ console.log(`\n🗂 StorageVisual Bridge v${VERSION}`)
251
+ console.log(` Listening on http://localhost:${PORT}`)
252
+ console.log(` Platform: ${process.platform} ${os.arch()}`)
253
+ console.log(` Only accepts requests from storagevisual.com\n`)
254
+ installAutoStart()
255
+ })
256
+
257
+ server.on('error', (err) => {
258
+ if (err.code === 'EADDRINUSE') {
259
+ console.log(`⚠️ Port ${PORT} already in use — bridge may already be running.`)
260
+ process.exit(0)
261
+ } else {
262
+ console.error('Bridge error:', err)
263
+ process.exit(1)
264
+ }
265
+ })
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require('./server.js')
package/svbridge ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require('./server.js')