9drive 1.0.1 → 1.0.3
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.
- package/README.md +432 -389
- package/backend/dist/modules/system/system.routes.js +57 -0
- package/backend/prisma/schema.sqlite.prisma +354 -354
- package/backend/public/assets/index-CriZZwHi.css +2 -0
- package/backend/public/assets/{index-EnIl3ceg.js → index-sAYu4Fpp.js} +2 -2
- package/backend/public/assets/{ort.bundle.min-B3pxUZZp.js → ort.bundle.min-DJ4PiNdC.js} +1 -1
- package/backend/public/assets/{ort.webgpu.bundle.min-oJmxMdTL.js → ort.webgpu.bundle.min-BmjguWk-.js} +1 -1
- package/backend/public/index.html +2 -2
- package/backend/public/sw.js +1 -1
- package/bin/9drive.js +423 -320
- package/package.json +60 -60
- package/backend/public/assets/index-xyDAj4M7.css +0 -2
package/bin/9drive.js
CHANGED
|
@@ -1,320 +1,423 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 9Drive CLI Launcher
|
|
5
|
-
* Zero-config Local-First Storage Cloud Server
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const fs = require('fs')
|
|
9
|
-
const path = require('path')
|
|
10
|
-
const os = require('os')
|
|
11
|
-
const crypto = require('crypto')
|
|
12
|
-
const { spawn, execSync } = require('child_process')
|
|
13
|
-
|
|
14
|
-
// Version from root package.json
|
|
15
|
-
let version = '1.0.0'
|
|
16
|
-
try {
|
|
17
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'))
|
|
18
|
-
if (pkg.version) version = pkg.version
|
|
19
|
-
} catch {}
|
|
20
|
-
|
|
21
|
-
const BANNER = `
|
|
22
|
-
\x1b[36m ___
|
|
23
|
-
\x1b[36m
|
|
24
|
-
\x1b[36m
|
|
25
|
-
\x1b[36m
|
|
26
|
-
\x1b[
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
console.log(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
--
|
|
45
|
-
--
|
|
46
|
-
--
|
|
47
|
-
--
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
npx 9drive
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
let
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
console.log(
|
|
150
|
-
console.log(
|
|
151
|
-
console.log(`
|
|
152
|
-
console.log(`
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
console.log(BANNER)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 9Drive CLI Launcher
|
|
5
|
+
* Zero-config Local-First Storage Cloud Server
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs')
|
|
9
|
+
const path = require('path')
|
|
10
|
+
const os = require('os')
|
|
11
|
+
const crypto = require('crypto')
|
|
12
|
+
const { spawn, execSync } = require('child_process')
|
|
13
|
+
|
|
14
|
+
// Version from root package.json
|
|
15
|
+
let version = '1.0.0'
|
|
16
|
+
try {
|
|
17
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf-8'))
|
|
18
|
+
if (pkg.version) version = pkg.version
|
|
19
|
+
} catch {}
|
|
20
|
+
|
|
21
|
+
const BANNER = `
|
|
22
|
+
\x1b[36m _ ___ ____ _ \x1b[0m
|
|
23
|
+
\x1b[36m / | / (_)___ ___ / __ \\_____(_) _____ \x1b[0m
|
|
24
|
+
\x1b[36m / |/ / / __ \\/ _ \\/ / / / ___/ / | / / _ \\\x1b[0m
|
|
25
|
+
\x1b[36m / /| / / / / / __/ /_/ / / / /| |/ / __/\x1b[0m
|
|
26
|
+
\x1b[36m/_/ |_/_/_/ /_/\\___/_____/_/ /_/ |___/\\___/ \x1b[33mv${version}\x1b[0m
|
|
27
|
+
\x1b[90m Cloud Storage & Universal API Gateway\x1b[0m
|
|
28
|
+
`
|
|
29
|
+
|
|
30
|
+
function printHelp() {
|
|
31
|
+
console.log(BANNER)
|
|
32
|
+
console.log(`
|
|
33
|
+
Usage:
|
|
34
|
+
9drive [command] [options]
|
|
35
|
+
|
|
36
|
+
Commands:
|
|
37
|
+
start Start the 9Drive local server (default)
|
|
38
|
+
status Show 9Drive data directory, database, and configuration
|
|
39
|
+
shortcut Create a Desktop shortcut / launcher (Windows, macOS, Linux)
|
|
40
|
+
backup Create an instant backup of the local database
|
|
41
|
+
open Open the 9Drive Web UI in your default browser
|
|
42
|
+
|
|
43
|
+
Options:
|
|
44
|
+
--port <num> Port number to run 9Drive on (default: 9999 or from config)
|
|
45
|
+
--host <str> Host to bind server (default: localhost)
|
|
46
|
+
--db <url> Custom database URL (SQLite file:... or MySQL mysql://...)
|
|
47
|
+
--no-open Do not open the browser automatically upon launch
|
|
48
|
+
--help, -h Show this help message
|
|
49
|
+
--version, -v Show version number
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
npx 9drive
|
|
53
|
+
npm i -g 9drive && 9drive shortcut
|
|
54
|
+
9drive start --port 8080
|
|
55
|
+
9drive status
|
|
56
|
+
9drive shortcut
|
|
57
|
+
`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Parse command line arguments
|
|
61
|
+
const args = process.argv.slice(2)
|
|
62
|
+
let command = 'start'
|
|
63
|
+
let portArg = null
|
|
64
|
+
let dbArg = null
|
|
65
|
+
let noOpen = false
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < args.length; i++) {
|
|
68
|
+
const arg = args[i]
|
|
69
|
+
if (arg === '--help' || arg === '-h') {
|
|
70
|
+
printHelp()
|
|
71
|
+
process.exit(0)
|
|
72
|
+
}
|
|
73
|
+
if (arg === '--version' || arg === '-v') {
|
|
74
|
+
console.log(`9Drive v${version}`)
|
|
75
|
+
process.exit(0)
|
|
76
|
+
}
|
|
77
|
+
if (arg === '--port') {
|
|
78
|
+
portArg = Number(args[++i])
|
|
79
|
+
} else if (arg === '--db') {
|
|
80
|
+
dbArg = args[++i]
|
|
81
|
+
} else if (arg === '--no-open') {
|
|
82
|
+
noOpen = true
|
|
83
|
+
} else if (!arg.startsWith('-') && i === 0) {
|
|
84
|
+
command = arg
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Setup user data directory: ~/.9drive/
|
|
89
|
+
const homeDir = os.homedir()
|
|
90
|
+
const dataDir = process.env.NINEDRIVE_DATA_DIR || path.join(homeDir, '.9drive')
|
|
91
|
+
const backupsDir = path.join(dataDir, 'backups')
|
|
92
|
+
const storageDir = path.join(dataDir, 'storage')
|
|
93
|
+
const configPath = path.join(dataDir, 'config.json')
|
|
94
|
+
const sqliteDbPath = path.join(dataDir, '9drive.db')
|
|
95
|
+
|
|
96
|
+
function ensureDirectories() {
|
|
97
|
+
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true })
|
|
98
|
+
if (!fs.existsSync(backupsDir)) fs.mkdirSync(backupsDir, { recursive: true })
|
|
99
|
+
if (!fs.existsSync(storageDir)) fs.mkdirSync(storageDir, { recursive: true })
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function loadOrCreateConfig() {
|
|
103
|
+
ensureDirectories()
|
|
104
|
+
let config = {}
|
|
105
|
+
if (fs.existsSync(configPath)) {
|
|
106
|
+
try {
|
|
107
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
108
|
+
} catch {}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const defaultPort = portArg || config.APP_PORT || 9999
|
|
112
|
+
const defaultDatabaseUrl = dbArg || config.DATABASE_URL || `file:${sqliteDbPath.replace(/\\/g, '/')}`
|
|
113
|
+
const defaultFrontendUrl = config.FRONTEND_URL || `http://localhost:${defaultPort}`
|
|
114
|
+
const defaultJwtSecret = config.JWT_ACCESS_SECRET || crypto.randomBytes(32).toString('hex')
|
|
115
|
+
const defaultEncryptionKey = config.TOKEN_ENCRYPTION_KEY || crypto.randomBytes(32).toString('hex')
|
|
116
|
+
|
|
117
|
+
const updatedConfig = {
|
|
118
|
+
APP_PORT: defaultPort,
|
|
119
|
+
DATABASE_URL: defaultDatabaseUrl,
|
|
120
|
+
FRONTEND_URL: defaultFrontendUrl,
|
|
121
|
+
JWT_ACCESS_SECRET: defaultJwtSecret,
|
|
122
|
+
TOKEN_ENCRYPTION_KEY: defaultEncryptionKey,
|
|
123
|
+
MAX_UPLOAD_BYTES: config.MAX_UPLOAD_BYTES || 5 * 1024 * 1024 * 1024,
|
|
124
|
+
...config,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (portArg) updatedConfig.APP_PORT = portArg
|
|
128
|
+
if (dbArg) updatedConfig.DATABASE_URL = dbArg
|
|
129
|
+
|
|
130
|
+
fs.writeFileSync(configPath, JSON.stringify(updatedConfig, null, 2))
|
|
131
|
+
return updatedConfig
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function openBrowser(url) {
|
|
135
|
+
const start =
|
|
136
|
+
process.platform === 'darwin'
|
|
137
|
+
? 'open'
|
|
138
|
+
: process.platform === 'win32'
|
|
139
|
+
? 'start'
|
|
140
|
+
: 'xdg-open'
|
|
141
|
+
try {
|
|
142
|
+
execSync(`${start} ${url}`, { stdio: 'ignore' })
|
|
143
|
+
} catch {}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function handleStatus() {
|
|
147
|
+
ensureDirectories()
|
|
148
|
+
const config = loadOrCreateConfig()
|
|
149
|
+
console.log(BANNER)
|
|
150
|
+
console.log('\x1b[32m[9Drive Status]\x1b[0m')
|
|
151
|
+
console.log(` Version: v${version}`)
|
|
152
|
+
console.log(` Data Directory: ${dataDir}`)
|
|
153
|
+
console.log(` Config File: ${configPath}`)
|
|
154
|
+
console.log(` Database URL: ${config.DATABASE_URL}`)
|
|
155
|
+
console.log(` Configured Port: ${config.APP_PORT}`)
|
|
156
|
+
|
|
157
|
+
if (fs.existsSync(sqliteDbPath)) {
|
|
158
|
+
const stats = fs.statSync(sqliteDbPath)
|
|
159
|
+
console.log(` SQLite DB Size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`)
|
|
160
|
+
} else {
|
|
161
|
+
console.log(` SQLite DB Size: Not initialized yet`)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const backups = fs.existsSync(backupsDir) ? fs.readdirSync(backupsDir) : []
|
|
165
|
+
console.log(` Total Backups: ${backups.length} snapshot(s)`)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function handleBackup() {
|
|
169
|
+
ensureDirectories()
|
|
170
|
+
if (!fs.existsSync(sqliteDbPath)) {
|
|
171
|
+
console.log('\x1b[33m[!] No SQLite database found to backup yet.\x1b[0m')
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
175
|
+
const destPath = path.join(backupsDir, `9drive-backup-${timestamp}.db`)
|
|
176
|
+
fs.copyFileSync(sqliteDbPath, destPath)
|
|
177
|
+
console.log(`\x1b[32m[✓] Database backup created:\x1b[0m ${destPath}`)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function handleShortcut() {
|
|
181
|
+
console.log(BANNER)
|
|
182
|
+
const home = os.homedir()
|
|
183
|
+
const platform = process.platform
|
|
184
|
+
|
|
185
|
+
if (platform === 'win32') {
|
|
186
|
+
const iconPath = path.join(__dirname, '../backend/public/favicon.ico')
|
|
187
|
+
|
|
188
|
+
const vbsScript = `
|
|
189
|
+
Set oWS = WScript.CreateObject("WScript.Shell")
|
|
190
|
+
sDesktop = oWS.SpecialFolders("Desktop")
|
|
191
|
+
sPrograms = oWS.SpecialFolders("Programs")
|
|
192
|
+
|
|
193
|
+
sLinkFile = sDesktop & "\\9Drive.lnk"
|
|
194
|
+
Set oLink = oWS.CreateShortcut(sLinkFile)
|
|
195
|
+
oLink.TargetPath = "cmd.exe"
|
|
196
|
+
oLink.Arguments = "/c 9drive"
|
|
197
|
+
oLink.Description = "9Drive Personal Cloud Storage"
|
|
198
|
+
oLink.WorkingDirectory = "${home.replace(/\\/g, '\\\\')}"
|
|
199
|
+
${fs.existsSync(iconPath) ? `oLink.IconLocation = "${iconPath.replace(/\\/g, '\\\\')}"` : ''}
|
|
200
|
+
oLink.Save
|
|
201
|
+
|
|
202
|
+
On Error Resume Next
|
|
203
|
+
sLinkFile2 = sPrograms & "\\9Drive.lnk"
|
|
204
|
+
Set oLink2 = oWS.CreateShortcut(sLinkFile2)
|
|
205
|
+
oLink2.TargetPath = "cmd.exe"
|
|
206
|
+
oLink2.Arguments = "/c 9drive"
|
|
207
|
+
oLink2.Description = "9Drive Personal Cloud Storage"
|
|
208
|
+
oLink2.WorkingDirectory = "${home.replace(/\\/g, '\\\\')}"
|
|
209
|
+
${fs.existsSync(iconPath) ? `oLink2.IconLocation = "${iconPath.replace(/\\/g, '\\\\')}"` : ''}
|
|
210
|
+
oLink2.Save
|
|
211
|
+
|
|
212
|
+
WScript.Echo sDesktop
|
|
213
|
+
`
|
|
214
|
+
|
|
215
|
+
const tempVbs = path.join(os.tmpdir(), `create-9drive-shortcut-${Date.now()}.vbs`)
|
|
216
|
+
try {
|
|
217
|
+
fs.writeFileSync(tempVbs, vbsScript)
|
|
218
|
+
const output = execSync(`cscript //nologo "${tempVbs}"`, { encoding: 'utf8' }).trim()
|
|
219
|
+
try { fs.unlinkSync(tempVbs) } catch {}
|
|
220
|
+
console.log(' \x1b[32m[✓]\x1b[0m Shortcut Desktop Windows berhasil dipasang!')
|
|
221
|
+
console.log(` Lokasi Desktop: \x1b[36m${output}\\9Drive.lnk\x1b[0m`)
|
|
222
|
+
console.log(' Sekarang Anda cukup double-click icon "9Drive" di Desktop untuk membuka aplikasi!\n')
|
|
223
|
+
} catch (err) {
|
|
224
|
+
console.log(' \x1b[31m[x]\x1b[0m Gagal membuat shortcut:', err.message)
|
|
225
|
+
}
|
|
226
|
+
} else if (platform === 'darwin') {
|
|
227
|
+
const desktopPath = path.join(home, 'Desktop')
|
|
228
|
+
const commandPath = path.join(desktopPath, '9Drive.command')
|
|
229
|
+
const scriptContent = `#!/bin/bash\n9drive\n`
|
|
230
|
+
try {
|
|
231
|
+
fs.writeFileSync(commandPath, scriptContent)
|
|
232
|
+
fs.chmodSync(commandPath, '755')
|
|
233
|
+
console.log(' \x1b[32m[✓]\x1b[0m macOS Desktop launcher created successfully!')
|
|
234
|
+
console.log(` Location: \x1b[36m${commandPath}\x1b[0m`)
|
|
235
|
+
console.log(' Double-click the 9Drive.command file to launch!\n')
|
|
236
|
+
} catch (err) {
|
|
237
|
+
console.log(' \x1b[31m[x]\x1b[0m Failed to create shortcut:', err.message)
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
const desktopDirs = [
|
|
241
|
+
path.join(home, '.local/share/applications'),
|
|
242
|
+
path.join(home, 'Desktop'),
|
|
243
|
+
]
|
|
244
|
+
const desktopContent = `[Desktop Entry]
|
|
245
|
+
Name=9Drive
|
|
246
|
+
Comment=Personal Cloud Storage & Universal API Gateway
|
|
247
|
+
Exec=9drive
|
|
248
|
+
Terminal=false
|
|
249
|
+
Type=Application
|
|
250
|
+
Categories=Network;FileTransfer;Utility;
|
|
251
|
+
`
|
|
252
|
+
let created = false
|
|
253
|
+
for (const dir of desktopDirs) {
|
|
254
|
+
if (fs.existsSync(dir)) {
|
|
255
|
+
const file = path.join(dir, '9drive.desktop')
|
|
256
|
+
try {
|
|
257
|
+
fs.writeFileSync(file, desktopContent)
|
|
258
|
+
fs.chmodSync(file, '755')
|
|
259
|
+
created = true
|
|
260
|
+
console.log(` \x1b[32m[✓]\x1b[0m Linux launcher created: \x1b[36m${file}\x1b[0m`)
|
|
261
|
+
} catch {}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (created) {
|
|
265
|
+
console.log(' 9Drive is now available in your application launcher!\n')
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function handleStart() {
|
|
271
|
+
ensureDirectories()
|
|
272
|
+
const config = loadOrCreateConfig()
|
|
273
|
+
|
|
274
|
+
console.log(BANNER)
|
|
275
|
+
console.log(` \x1b[32m[✓]\x1b[0m Data directory: \x1b[90m${dataDir}\x1b[0m`)
|
|
276
|
+
|
|
277
|
+
const shortcutCheck = process.platform === 'win32'
|
|
278
|
+
? path.join(os.homedir(), 'Desktop/9Drive.lnk')
|
|
279
|
+
: path.join(os.homedir(), 'Desktop/9Drive.command')
|
|
280
|
+
if (!fs.existsSync(shortcutCheck)) {
|
|
281
|
+
console.log(` \x1b[33m[💡 Tip]\x1b[0m Ingin pasang icon di Desktop? Jalankan: \x1b[36m9drive shortcut\x1b[0m`)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const isSqlite = config.DATABASE_URL.startsWith('file:') || config.DATABASE_URL.startsWith('sqlite:')
|
|
285
|
+
|
|
286
|
+
// Auto-backup database on startup if existing SQLite database found
|
|
287
|
+
if (isSqlite && fs.existsSync(sqliteDbPath)) {
|
|
288
|
+
try {
|
|
289
|
+
const stats = fs.statSync(sqliteDbPath)
|
|
290
|
+
if (stats.size > 0) {
|
|
291
|
+
const timestamp = new Date().toISOString().slice(0, 10)
|
|
292
|
+
const autoBackup = path.join(backupsDir, `auto-backup-${timestamp}.db`)
|
|
293
|
+
fs.copyFileSync(sqliteDbPath, autoBackup)
|
|
294
|
+
}
|
|
295
|
+
} catch {}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Resolve backend root and prisma
|
|
299
|
+
const backendDir = path.resolve(__dirname, '../backend')
|
|
300
|
+
const rootDir = path.resolve(__dirname, '..')
|
|
301
|
+
|
|
302
|
+
// Sync SQLite schema automatically if in SQLite mode
|
|
303
|
+
if (isSqlite) {
|
|
304
|
+
const sqliteSchemaPath = path.join(backendDir, 'prisma/schema.sqlite.prisma')
|
|
305
|
+
const sqliteClientIndex = path.join(backendDir, 'prisma/generated/sqlite-client/index.js')
|
|
306
|
+
const prismaCli = [
|
|
307
|
+
path.join(rootDir, 'node_modules/prisma/build/index.js'),
|
|
308
|
+
path.join(backendDir, 'node_modules/prisma/build/index.js'),
|
|
309
|
+
].find((p) => fs.existsSync(p)) || null
|
|
310
|
+
|
|
311
|
+
if (fs.existsSync(sqliteSchemaPath) && prismaCli) {
|
|
312
|
+
if (!fs.existsSync(sqliteClientIndex)) {
|
|
313
|
+
process.stdout.write(' [⧗] Generating SQLite database client... ')
|
|
314
|
+
try {
|
|
315
|
+
execSync(`node "${prismaCli}" generate --schema="${sqliteSchemaPath}"`, {
|
|
316
|
+
stdio: 'ignore',
|
|
317
|
+
env: { ...process.env, DATABASE_URL: config.DATABASE_URL },
|
|
318
|
+
})
|
|
319
|
+
console.log('\x1b[32m[Ready]\x1b[0m')
|
|
320
|
+
} catch {
|
|
321
|
+
console.log('\x1b[33m[Notice: Generator skipped]\x1b[0m')
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
process.stdout.write(' [⧗] Checking database schema... ')
|
|
326
|
+
try {
|
|
327
|
+
execSync(
|
|
328
|
+
`node "${prismaCli}" db push --schema="${sqliteSchemaPath}" --skip-generate`,
|
|
329
|
+
{
|
|
330
|
+
stdio: 'ignore',
|
|
331
|
+
env: {
|
|
332
|
+
...process.env,
|
|
333
|
+
DATABASE_URL: config.DATABASE_URL,
|
|
334
|
+
},
|
|
335
|
+
}
|
|
336
|
+
)
|
|
337
|
+
console.log('\x1b[32m[Ready]\x1b[0m')
|
|
338
|
+
} catch (err) {
|
|
339
|
+
console.log('\x1b[33m[Notice: Using existing schema]\x1b[0m')
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Locate server entry file (dist/server.js or src/server.ts)
|
|
345
|
+
let serverEntry = path.join(backendDir, 'dist/server.js')
|
|
346
|
+
let useTsx = false
|
|
347
|
+
|
|
348
|
+
if (!fs.existsSync(serverEntry)) {
|
|
349
|
+
serverEntry = path.join(backendDir, 'src/server.ts')
|
|
350
|
+
useTsx = true
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const serverUrl = `http://localhost:${config.APP_PORT}`
|
|
354
|
+
console.log(` \x1b[32m[✓]\x1b[0m Web Dashboard: \x1b[36m\x1b[4m${serverUrl}\x1b[0m`)
|
|
355
|
+
console.log(` \x1b[90m[i] Press Ctrl+C to stop the server.\x1b[0m\n`)
|
|
356
|
+
|
|
357
|
+
const nodePath = [
|
|
358
|
+
path.join(backendDir, 'node_modules'),
|
|
359
|
+
path.join(rootDir, 'node_modules'),
|
|
360
|
+
process.env.NODE_PATH || '',
|
|
361
|
+
].filter(Boolean).join(path.delimiter)
|
|
362
|
+
|
|
363
|
+
const envVars = {
|
|
364
|
+
...process.env,
|
|
365
|
+
NODE_PATH: nodePath,
|
|
366
|
+
NINEDRIVE_CLI: 'true',
|
|
367
|
+
NINEDRIVE_DATA_DIR: dataDir,
|
|
368
|
+
APP_PORT: String(config.APP_PORT),
|
|
369
|
+
DATABASE_URL: config.DATABASE_URL,
|
|
370
|
+
FRONTEND_URL: config.FRONTEND_URL || serverUrl,
|
|
371
|
+
JWT_ACCESS_SECRET: config.JWT_ACCESS_SECRET,
|
|
372
|
+
TOKEN_ENCRYPTION_KEY: config.TOKEN_ENCRYPTION_KEY,
|
|
373
|
+
SERVE_FRONTEND: 'true',
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const runner = useTsx
|
|
377
|
+
? path.join(backendDir, 'node_modules/tsx/dist/cli.mjs')
|
|
378
|
+
: null
|
|
379
|
+
|
|
380
|
+
const child = runner
|
|
381
|
+
? spawn('node', [runner, serverEntry], {
|
|
382
|
+
cwd: backendDir,
|
|
383
|
+
env: envVars,
|
|
384
|
+
stdio: 'inherit',
|
|
385
|
+
})
|
|
386
|
+
: spawn('node', [serverEntry], {
|
|
387
|
+
cwd: backendDir,
|
|
388
|
+
env: envVars,
|
|
389
|
+
stdio: 'inherit',
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
// Open browser after short delay
|
|
393
|
+
if (!noOpen) {
|
|
394
|
+
setTimeout(() => {
|
|
395
|
+
openBrowser(serverUrl)
|
|
396
|
+
}, 1500)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
child.on('exit', (code) => {
|
|
400
|
+
process.exit(code || 0)
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Route commands
|
|
405
|
+
switch (command) {
|
|
406
|
+
case 'status':
|
|
407
|
+
handleStatus()
|
|
408
|
+
break
|
|
409
|
+
case 'shortcut':
|
|
410
|
+
handleShortcut()
|
|
411
|
+
break
|
|
412
|
+
case 'backup':
|
|
413
|
+
handleBackup()
|
|
414
|
+
break
|
|
415
|
+
case 'open':
|
|
416
|
+
const conf = loadOrCreateConfig()
|
|
417
|
+
openBrowser(`http://localhost:${conf.APP_PORT}`)
|
|
418
|
+
break
|
|
419
|
+
case 'start':
|
|
420
|
+
default:
|
|
421
|
+
handleStart()
|
|
422
|
+
break
|
|
423
|
+
}
|