@wwkit/opm 1.0.3 → 1.0.5
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/package.json +2 -2
- package/src/cli/groups/ping.js +2 -0
- package/src/cli/index.js +4 -0
- package/src/config.json5 +18 -0
- package/src/index.js +2 -0
- package/src/managers/npm.js +28 -0
- package/src/tools/share/.htaccess +20 -0
- package/src/tools/share/client.js +172 -0
- package/src/tools/share/index.js +735 -0
- package/src/tools/share/server.js +430 -0
- package/src/tools/share/server.php +452 -0
- package/src/tools/webwork/index.js +716 -0
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opm share 命令组 — 文本分享服务
|
|
3
|
+
*
|
|
4
|
+
* 命令:upload / zip / get / view / open / list / delete / clear / config / server / help
|
|
5
|
+
* 服务地址由 config.share.server.active 解析(不支持命令行 -s 参数)。
|
|
6
|
+
* 需要服务调用的命令执行前先 ping 验证可达性;
|
|
7
|
+
* 若 active=local 且服务未启动,自动后台启动后再执行。
|
|
8
|
+
* server 子命令:start(后台启动)/ stop(停止)/ status(查询状态)。
|
|
9
|
+
* zip 上传文件/文件夹(压缩为 zip 后 base64 上传),get -d 下载并解压。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'node:fs'
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import os from 'node:os'
|
|
15
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
16
|
+
import { parseFlags } from '../../cli/helpers/args.js'
|
|
17
|
+
import { getConfig, getSection, getUserConfigDir } from '../../config.js'
|
|
18
|
+
import { output } from '../../formatter.js'
|
|
19
|
+
import { getShareServer, pingServer, uploadShare, getShare, getLatestShare, getHtmlUrl, listShares, deleteShare, clearShares } from './client.js'
|
|
20
|
+
import { startServer } from './server.js'
|
|
21
|
+
|
|
22
|
+
const DEFAULT_PASSWORD = '0000'
|
|
23
|
+
const ZIP_TMP_PREFIX = 'opm-share-'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 生成默认标题(内容前 30 字符折叠空白 + ...,≤30 用原文)
|
|
27
|
+
* @param {string} content
|
|
28
|
+
* @returns {string}
|
|
29
|
+
*/
|
|
30
|
+
function defaultTitle(content) {
|
|
31
|
+
const folded = content.replace(/\s+/g, ' ').trim()
|
|
32
|
+
if (folded.length <= 30) return folded
|
|
33
|
+
return folded.slice(0, 30) + '...'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 展开 ~ 路径
|
|
38
|
+
* @param {string} p
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
function expandTilde(p) {
|
|
42
|
+
if (p && p.startsWith('~/')) {
|
|
43
|
+
return path.join(os.homedir(), p.slice(2))
|
|
44
|
+
}
|
|
45
|
+
return p
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 浏览器打开 URL
|
|
50
|
+
* @param {string} url
|
|
51
|
+
*/
|
|
52
|
+
function openBrowser(url) {
|
|
53
|
+
const platform = process.platform
|
|
54
|
+
let cmd
|
|
55
|
+
if (platform === 'darwin') {
|
|
56
|
+
cmd = `open "${url}"`
|
|
57
|
+
} else if (platform === 'win32') {
|
|
58
|
+
cmd = `start "" "${url}"`
|
|
59
|
+
} else {
|
|
60
|
+
cmd = `xdg-open "${url}"`
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
spawnSync(cmd, { shell: true, stdio: 'ignore' })
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 获取 server 状态文件路径
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
function getServePidFile() {
|
|
72
|
+
return path.join(getUserConfigDir(), 'share', 'serve.pid')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 读取 server PID 文件
|
|
77
|
+
* @returns {{pid: number, url: string, host: string, port: number, dataDir: string, started: string}|null}
|
|
78
|
+
*/
|
|
79
|
+
function readServePid() {
|
|
80
|
+
const pidFile = getServePidFile()
|
|
81
|
+
if (!fs.existsSync(pidFile)) return null
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(fs.readFileSync(pidFile, 'utf8'))
|
|
84
|
+
} catch {
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 检查进程是否存活
|
|
91
|
+
* @param {number} pid
|
|
92
|
+
* @returns {boolean}
|
|
93
|
+
*/
|
|
94
|
+
function isProcessAlive(pid) {
|
|
95
|
+
if (!pid) return false
|
|
96
|
+
try {
|
|
97
|
+
process.kill(pid, 0)
|
|
98
|
+
return true
|
|
99
|
+
} catch {
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const ACTIONS = {
|
|
105
|
+
upload: { desc: 'Upload text content (positional, -f file, or stdin; -i for custom id)' },
|
|
106
|
+
zip: { desc: 'Upload file/folder as compressed zip (get -d to extract; -i for custom id)' },
|
|
107
|
+
get: { desc: 'Get shared content (id optional; defaults to latest; -d to extract zip)' },
|
|
108
|
+
view: { desc: 'View shared content (id optional; defaults to latest)' },
|
|
109
|
+
open: { desc: 'Open shared content in browser (id optional; defaults to latest)' },
|
|
110
|
+
list: { desc: 'List recent shares (-n count, default 10)' },
|
|
111
|
+
delete: { desc: 'Delete a share by id' },
|
|
112
|
+
clear: { desc: 'Clean up shares by days and/or keep count' },
|
|
113
|
+
config: { desc: 'Show current share config section' },
|
|
114
|
+
server: { desc: 'Manage local server (start/stop/status)' },
|
|
115
|
+
help: { desc: 'Show this help' },
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export class ShareGroup {
|
|
119
|
+
constructor() {
|
|
120
|
+
this.name = 'share'
|
|
121
|
+
this.desc = 'Text sharing service (upload/get/open/list/server)'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async run(argv) {
|
|
125
|
+
const [action, ...rest] = argv
|
|
126
|
+
|
|
127
|
+
if (!action || action === '-h' || action === '--help' || action === 'help') {
|
|
128
|
+
this.printHelp()
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const parsed = parseFlags(rest)
|
|
133
|
+
|
|
134
|
+
switch (action) {
|
|
135
|
+
case 'upload':
|
|
136
|
+
return this._upload(parsed)
|
|
137
|
+
case 'zip':
|
|
138
|
+
return this._zip(parsed)
|
|
139
|
+
case 'get':
|
|
140
|
+
return this._get(parsed)
|
|
141
|
+
case 'view':
|
|
142
|
+
return this._view(parsed)
|
|
143
|
+
case 'open':
|
|
144
|
+
return this._open(parsed)
|
|
145
|
+
case 'list':
|
|
146
|
+
return this._list(parsed)
|
|
147
|
+
case 'delete':
|
|
148
|
+
return this._delete(parsed)
|
|
149
|
+
case 'clear':
|
|
150
|
+
return this._clear(parsed)
|
|
151
|
+
case 'config':
|
|
152
|
+
return this._config(parsed)
|
|
153
|
+
case 'server':
|
|
154
|
+
return this._server(parsed)
|
|
155
|
+
default:
|
|
156
|
+
return this._upload(parseFlags([action, ...rest]))
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
_getShareConfig() {
|
|
161
|
+
return getSection(getConfig(), 'share') || {}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_getServerUrl() {
|
|
165
|
+
return getShareServer(this._getShareConfig())
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
_isLocalActive() {
|
|
169
|
+
const cfg = this._getShareConfig()
|
|
170
|
+
const server = cfg.server || {}
|
|
171
|
+
return server.active === 'local'
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_getServeOpts() {
|
|
175
|
+
const cfg = this._getShareConfig()
|
|
176
|
+
const serve = cfg.serve || {}
|
|
177
|
+
return {
|
|
178
|
+
host: serve.host || '0.0.0.0',
|
|
179
|
+
port: serve.port || 8787,
|
|
180
|
+
dataDir: expandTilde(serve.dataDir || '~/.config/opm/share/data'),
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 解析密码:-p/--password 参数 > 配置 share.password > 位置参数(get/open)> 默认 0000
|
|
186
|
+
* @param {object} parsed - parseFlags 结果
|
|
187
|
+
* @param {string} [positional] - 位置参数密码(get/open 的第二个位置参数)
|
|
188
|
+
* @returns {string}
|
|
189
|
+
*/
|
|
190
|
+
_resolvePassword(parsed, positional = '') {
|
|
191
|
+
const flag = parsed.flags.p || parsed.flags.password || ''
|
|
192
|
+
if (flag && flag !== 'true') return flag
|
|
193
|
+
const cfgPassword = this._getShareConfig().password
|
|
194
|
+
if (cfgPassword) return cfgPassword
|
|
195
|
+
return positional || DEFAULT_PASSWORD
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 确保服务可达:ping 不通且 active=local 时自动后台启动
|
|
200
|
+
* @returns {Promise<string>} baseUrl
|
|
201
|
+
*/
|
|
202
|
+
async _ensureReachable() {
|
|
203
|
+
const baseUrl = this._getServerUrl()
|
|
204
|
+
if (!baseUrl) {
|
|
205
|
+
console.error('[opm share] No server configured. Run "opm share config" to check.')
|
|
206
|
+
process.exit(1)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const result = await pingServer(baseUrl)
|
|
210
|
+
if (result.reachable) return baseUrl
|
|
211
|
+
|
|
212
|
+
if (!this._isLocalActive()) {
|
|
213
|
+
console.error(`[opm share] Server unreachable: ${baseUrl}`)
|
|
214
|
+
console.error(`[opm share] ${result.message}`)
|
|
215
|
+
console.error('[opm share] Run "opm share config" to check.')
|
|
216
|
+
process.exit(1)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.error('[opm share] Local server not running, starting...')
|
|
220
|
+
this._startBackground()
|
|
221
|
+
|
|
222
|
+
for (let i = 0; i < 10; i++) {
|
|
223
|
+
await new Promise((r) => setTimeout(r, 300))
|
|
224
|
+
const recheck = await pingServer(baseUrl)
|
|
225
|
+
if (recheck.reachable) {
|
|
226
|
+
console.error('[opm share] Server started.')
|
|
227
|
+
return baseUrl
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
console.error(`[opm share] Failed to start local server. Run "opm share server status" to check.`)
|
|
232
|
+
process.exit(1)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async _upload(parsed) {
|
|
236
|
+
const title = parsed.flags.t || parsed.flags.title || ''
|
|
237
|
+
const file = parsed.flags.f || parsed.flags.file || ''
|
|
238
|
+
const id = parsed.flags.i || parsed.flags.id || ''
|
|
239
|
+
const password = this._resolvePassword(parsed)
|
|
240
|
+
|
|
241
|
+
let content = ''
|
|
242
|
+
|
|
243
|
+
if (file) {
|
|
244
|
+
const resolved = path.resolve(file)
|
|
245
|
+
if (!fs.existsSync(resolved)) {
|
|
246
|
+
console.error(`File not found: ${resolved}`)
|
|
247
|
+
process.exit(1)
|
|
248
|
+
}
|
|
249
|
+
content = fs.readFileSync(resolved, 'utf8')
|
|
250
|
+
} else if (parsed.positional.length > 0) {
|
|
251
|
+
content = parsed.positional.join(' ')
|
|
252
|
+
} else if (!process.stdin.isTTY) {
|
|
253
|
+
content = fs.readFileSync(0, 'utf8')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!content) {
|
|
257
|
+
console.error('Usage: opm share upload "<content>" [-t title] [-f file] [-i id] [-p password]')
|
|
258
|
+
console.error(' Or: echo "..." | opm share upload [-t title] [-i id] [-p password]')
|
|
259
|
+
console.error(' Or: opm share "<content>" [-t title] [-i id] [-p password] (shorthand without "upload")')
|
|
260
|
+
process.exit(1)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const baseUrl = await this._ensureReachable()
|
|
264
|
+
const result = await uploadShare(baseUrl, {
|
|
265
|
+
title: title || defaultTitle(content),
|
|
266
|
+
content,
|
|
267
|
+
password,
|
|
268
|
+
...(id ? { id } : {}),
|
|
269
|
+
})
|
|
270
|
+
output(result)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async _zip(parsed) {
|
|
274
|
+
const inputPath = parsed.positional[0] || parsed.flags.f || parsed.flags.file || ''
|
|
275
|
+
const title = parsed.flags.t || parsed.flags.title || ''
|
|
276
|
+
const id = parsed.flags.i || parsed.flags.id || ''
|
|
277
|
+
const password = this._resolvePassword(parsed)
|
|
278
|
+
|
|
279
|
+
if (!inputPath) {
|
|
280
|
+
console.error('Usage: opm share zip <path> [-t title] [-i id] [-p password]')
|
|
281
|
+
process.exit(1)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const resolved = path.resolve(inputPath)
|
|
285
|
+
if (!fs.existsSync(resolved)) {
|
|
286
|
+
console.error(`Path not found: ${resolved}`)
|
|
287
|
+
process.exit(1)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const stat = fs.statSync(resolved)
|
|
291
|
+
const isDir = stat.isDirectory()
|
|
292
|
+
if (isDir) {
|
|
293
|
+
const entries = fs.readdirSync(resolved)
|
|
294
|
+
if (entries.length === 0) {
|
|
295
|
+
console.error(`Directory is empty: ${resolved}`)
|
|
296
|
+
process.exit(1)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const baseName = path.basename(resolved)
|
|
301
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), ZIP_TMP_PREFIX))
|
|
302
|
+
const zipPath = path.join(tmpDir, `${baseName}.zip`)
|
|
303
|
+
|
|
304
|
+
const zipArgs = isDir ? ['-r', zipPath, baseName] : [zipPath, baseName]
|
|
305
|
+
const zipResult = spawnSync('zip', zipArgs, {
|
|
306
|
+
cwd: path.dirname(resolved),
|
|
307
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
if (zipResult.status !== 0) {
|
|
311
|
+
const stderr = zipResult.stderr ? zipResult.stderr.toString().trim() : 'unknown error'
|
|
312
|
+
console.error(`Zip failed: ${stderr}`)
|
|
313
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
314
|
+
process.exit(1)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const zipBuffer = fs.readFileSync(zipPath)
|
|
318
|
+
const content = zipBuffer.toString('base64')
|
|
319
|
+
|
|
320
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
321
|
+
|
|
322
|
+
const baseUrl = await this._ensureReachable()
|
|
323
|
+
const result = await uploadShare(baseUrl, {
|
|
324
|
+
title: title || `${baseName}.zip`,
|
|
325
|
+
content,
|
|
326
|
+
password,
|
|
327
|
+
type: 'zip',
|
|
328
|
+
filename: `${baseName}.zip`,
|
|
329
|
+
...(id ? { id } : {}),
|
|
330
|
+
})
|
|
331
|
+
output(result)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async _get(parsed) {
|
|
335
|
+
const id = parsed.positional[0] || ''
|
|
336
|
+
const password = this._resolvePassword(parsed, parsed.positional[1] || '')
|
|
337
|
+
const extractDir = parsed.flags.d || parsed.flags.dir || ''
|
|
338
|
+
|
|
339
|
+
const baseUrl = await this._ensureReachable()
|
|
340
|
+
const result = id
|
|
341
|
+
? await getShare(baseUrl, id, password)
|
|
342
|
+
: await getLatestShare(baseUrl, password)
|
|
343
|
+
|
|
344
|
+
if (result.type === 'zip' && extractDir) {
|
|
345
|
+
await this._extractZip(result, extractDir)
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
output(result)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async _extractZip(result, dir) {
|
|
353
|
+
const resolvedDir = path.resolve(dir)
|
|
354
|
+
fs.mkdirSync(resolvedDir, { recursive: true })
|
|
355
|
+
|
|
356
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), ZIP_TMP_PREFIX))
|
|
357
|
+
const zipPath = path.join(tmpDir, result.filename || `${result.id}.zip`)
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const buf = Buffer.from(result.content, 'base64')
|
|
361
|
+
fs.writeFileSync(zipPath, buf)
|
|
362
|
+
|
|
363
|
+
const { default: extractZip } = await import('extract-zip')
|
|
364
|
+
await extractZip(zipPath, { dir: resolvedDir })
|
|
365
|
+
|
|
366
|
+
const files = this._listTopLevel(resolvedDir)
|
|
367
|
+
output({
|
|
368
|
+
id: result.id,
|
|
369
|
+
title: result.title,
|
|
370
|
+
type: 'zip',
|
|
371
|
+
filename: result.filename,
|
|
372
|
+
created: result.created,
|
|
373
|
+
extracted: true,
|
|
374
|
+
dir: resolvedDir,
|
|
375
|
+
files,
|
|
376
|
+
})
|
|
377
|
+
} catch (err) {
|
|
378
|
+
console.error(`Extract failed: ${err.message}`)
|
|
379
|
+
process.exit(1)
|
|
380
|
+
} finally {
|
|
381
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* 列出目录一级文件清单
|
|
387
|
+
* @param {string} dir
|
|
388
|
+
* @returns {Array<{name: string, type: string}>}
|
|
389
|
+
*/
|
|
390
|
+
_listTopLevel(dir) {
|
|
391
|
+
try {
|
|
392
|
+
return fs.readdirSync(dir, { withFileTypes: true }).map((e) => ({
|
|
393
|
+
name: e.name,
|
|
394
|
+
type: e.isDirectory() ? 'dir' : 'file',
|
|
395
|
+
}))
|
|
396
|
+
} catch {
|
|
397
|
+
return []
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async _view(parsed) {
|
|
402
|
+
const id = parsed.positional[0] || ''
|
|
403
|
+
const password = this._resolvePassword(parsed, parsed.positional[1] || '')
|
|
404
|
+
|
|
405
|
+
const baseUrl = await this._ensureReachable()
|
|
406
|
+
const result = id
|
|
407
|
+
? await getShare(baseUrl, id, password)
|
|
408
|
+
: await getLatestShare(baseUrl, password)
|
|
409
|
+
|
|
410
|
+
if (result.type === 'zip') {
|
|
411
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), ZIP_TMP_PREFIX))
|
|
412
|
+
try {
|
|
413
|
+
const zipPath = path.join(tmpDir, result.filename || `${result.id}.zip`)
|
|
414
|
+
const buf = Buffer.from(result.content, 'base64')
|
|
415
|
+
fs.writeFileSync(zipPath, buf)
|
|
416
|
+
|
|
417
|
+
const { default: extractZip } = await import('extract-zip')
|
|
418
|
+
await extractZip(zipPath, { dir: tmpDir })
|
|
419
|
+
|
|
420
|
+
fs.unlinkSync(zipPath)
|
|
421
|
+
|
|
422
|
+
const files = this._listTopLevel(tmpDir)
|
|
423
|
+
for (const f of files) {
|
|
424
|
+
console.log(`${f.type === 'dir' ? 'd' : 'f'} ${f.name}`)
|
|
425
|
+
}
|
|
426
|
+
} catch (err) {
|
|
427
|
+
console.error(`View zip failed: ${err.message}`)
|
|
428
|
+
process.exit(1)
|
|
429
|
+
} finally {
|
|
430
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
431
|
+
}
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
process.stdout.write(result.content || '')
|
|
436
|
+
if (!(result.content || '').endsWith('\n')) {
|
|
437
|
+
process.stdout.write('\n')
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async _open(parsed) {
|
|
442
|
+
const id = parsed.positional[0] || ''
|
|
443
|
+
const password = this._resolvePassword(parsed, parsed.positional[1] || '')
|
|
444
|
+
|
|
445
|
+
const baseUrl = await this._ensureReachable()
|
|
446
|
+
|
|
447
|
+
if (id) {
|
|
448
|
+
const htmlUrl = getHtmlUrl(baseUrl, id, password)
|
|
449
|
+
openBrowser(htmlUrl)
|
|
450
|
+
output({ opened: true, url: htmlUrl })
|
|
451
|
+
} else {
|
|
452
|
+
const result = await getLatestShare(baseUrl, password)
|
|
453
|
+
const htmlUrl = getHtmlUrl(baseUrl, result.id, password)
|
|
454
|
+
openBrowser(htmlUrl)
|
|
455
|
+
output({ opened: true, id: result.id, url: htmlUrl })
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async _list(parsed) {
|
|
460
|
+
const n = parseInt(parsed.flags.n || parsed.flags.count || '10', 10) || 10
|
|
461
|
+
|
|
462
|
+
const baseUrl = await this._ensureReachable()
|
|
463
|
+
const result = await listShares(baseUrl, n)
|
|
464
|
+
output(result)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async _delete(parsed) {
|
|
468
|
+
const id = parsed.positional[0]
|
|
469
|
+
if (!id) {
|
|
470
|
+
console.error('Usage: opm share delete <id>')
|
|
471
|
+
process.exit(1)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const baseUrl = await this._ensureReachable()
|
|
475
|
+
const result = await deleteShare(baseUrl, id)
|
|
476
|
+
output(result)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async _clear(parsed) {
|
|
480
|
+
const days = parseInt(parsed.flags.days || '0', 10) || 0
|
|
481
|
+
const keep = parseInt(parsed.flags.keep || parsed.flags.n || '0', 10) || 0
|
|
482
|
+
|
|
483
|
+
if (!days && !keep) {
|
|
484
|
+
console.error('Usage: opm share clear [--days <n>] [--keep <n>]')
|
|
485
|
+
console.error(' --days <n> Delete shares older than n days')
|
|
486
|
+
console.error(' --keep <n> Keep only the most recent n shares')
|
|
487
|
+
console.error(' Both can be combined: clear --days 7 --keep 50')
|
|
488
|
+
process.exit(1)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const baseUrl = await this._ensureReachable()
|
|
492
|
+
const result = await clearShares(baseUrl, { days, keep })
|
|
493
|
+
output(result)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
_config() {
|
|
497
|
+
const cfg = this._getShareConfig()
|
|
498
|
+
output(cfg)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async _server(parsed) {
|
|
502
|
+
const [sub] = parsed.positional
|
|
503
|
+
|
|
504
|
+
if (sub === 'start') {
|
|
505
|
+
return this._serverrStart()
|
|
506
|
+
}
|
|
507
|
+
if (sub === 'stop') {
|
|
508
|
+
return this._serverrStop()
|
|
509
|
+
}
|
|
510
|
+
if (sub === 'status' || !sub) {
|
|
511
|
+
return this._serverrStatus()
|
|
512
|
+
}
|
|
513
|
+
if (sub === 'help' || sub === '-h' || sub === '--help') {
|
|
514
|
+
this._serverrHelp()
|
|
515
|
+
return
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
console.error(`Unknown server subcommand: ${sub}`)
|
|
519
|
+
console.error('Usage: opm share server [start|stop|status|help]')
|
|
520
|
+
process.exit(1)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
_serverrHelp() {
|
|
524
|
+
console.log(`
|
|
525
|
+
Usage: opm share server <subcommand>
|
|
526
|
+
|
|
527
|
+
Subcommands:
|
|
528
|
+
start Start local server (background, reads host/port/dataDir from config)
|
|
529
|
+
stop Stop local server
|
|
530
|
+
status Check if server is running
|
|
531
|
+
help Show this help
|
|
532
|
+
|
|
533
|
+
Local development:
|
|
534
|
+
opm share server start Start in background (reads config)
|
|
535
|
+
opm share server status Check status
|
|
536
|
+
opm share server stop Stop server
|
|
537
|
+
opm share config Show current config (host/port/dataDir)
|
|
538
|
+
|
|
539
|
+
# Or run server.js directly (foreground):
|
|
540
|
+
node src/tools/share/server.js
|
|
541
|
+
OPM_SHARE_HOST=0.0.0.0 OPM_SHARE_PORT=9000 node src/tools/share/server.js
|
|
542
|
+
|
|
543
|
+
Deploy to server:
|
|
544
|
+
# 1. Install @wwkit/opm on the server
|
|
545
|
+
npm install -g @wwkit/opm
|
|
546
|
+
|
|
547
|
+
# 2. Configure share.server.active to the server's URL
|
|
548
|
+
# Edit ~/.config/opm/config.json5:
|
|
549
|
+
# share.server.active = "official" # or a custom URL
|
|
550
|
+
|
|
551
|
+
# 3. Start the server (foreground or via process manager)
|
|
552
|
+
opm share server start
|
|
553
|
+
# Or with custom env:
|
|
554
|
+
OPM_SHARE_HOST=0.0.0.0 OPM_SHARE_PORT=80 OPM_SHARE_DATA_DIR=/var/lib/opm/share node src/tools/share/server.js
|
|
555
|
+
|
|
556
|
+
# 4. Use systemd / pm2 / docker for production:
|
|
557
|
+
# pm2 start src/tools/share/server.js --name opm-share
|
|
558
|
+
# pm2 save && pm2 startup
|
|
559
|
+
|
|
560
|
+
# 5. Update client config to point to the server:
|
|
561
|
+
# opm share config # verify active points to the right URL
|
|
562
|
+
|
|
563
|
+
API endpoints:
|
|
564
|
+
POST /share Upload share
|
|
565
|
+
Body: {title?, content, password?, type?, filename?}
|
|
566
|
+
Response: {id, url, htmlUrl}
|
|
567
|
+
|
|
568
|
+
GET /share/<id> Browser -> HTML page; Accept: application/json -> JSON
|
|
569
|
+
GET /share/<id>/<password> Same with password
|
|
570
|
+
|
|
571
|
+
GET /share/html/<id> Always returns HTML page
|
|
572
|
+
GET /share/html/<id>/<password> Same with password
|
|
573
|
+
|
|
574
|
+
GET /share/list?n=10 List recent shares (default 10)
|
|
575
|
+
Response: {count, items: [{id, title, created}]}
|
|
576
|
+
|
|
577
|
+
POST /share/delete/<id> Delete a share by id
|
|
578
|
+
Response: {deleted: true, id}
|
|
579
|
+
|
|
580
|
+
POST /share/clear?days=7&keep=10 Clean up by age and/or count
|
|
581
|
+
Response: {deleted: number, ids: [string]}
|
|
582
|
+
|
|
583
|
+
Environment variables (for server.js direct execution):
|
|
584
|
+
OPM_SHARE_HOST Listen host (default: 127.0.0.1)
|
|
585
|
+
OPM_SHARE_PORT Listen port (default: 8787)
|
|
586
|
+
OPM_SHARE_DATA_DIR Data directory (default: ~/.config/opm/share/data)
|
|
587
|
+
`)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
_serverrStart() {
|
|
591
|
+
const existing = readServePid()
|
|
592
|
+
if (existing && isProcessAlive(existing.pid)) {
|
|
593
|
+
output({ running: true, pid: existing.pid, url: existing.url, host: existing.host, port: existing.port, dataDir: existing.dataDir, started: existing.started, message: 'Server already running' })
|
|
594
|
+
return
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
this._startBackground()
|
|
598
|
+
output({ started: true, message: 'Server started in background. Run "opm share server status" to check.' })
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
_startBackground() {
|
|
602
|
+
const opts = this._getServeOpts()
|
|
603
|
+
const pidFile = getServePidFile()
|
|
604
|
+
fs.mkdirSync(path.dirname(pidFile), { recursive: true })
|
|
605
|
+
|
|
606
|
+
const logFile = path.join(path.dirname(pidFile), 'serve.log')
|
|
607
|
+
const logFd = fs.openSync(logFile, 'a')
|
|
608
|
+
const child = spawn(process.execPath, [path.join(path.dirname(new URL(import.meta.url).pathname), 'server.js')], {
|
|
609
|
+
detached: true,
|
|
610
|
+
stdio: ['ignore', logFd, logFd],
|
|
611
|
+
env: {
|
|
612
|
+
...process.env,
|
|
613
|
+
OPM_SHARE_HOST: opts.host,
|
|
614
|
+
OPM_SHARE_PORT: String(opts.port),
|
|
615
|
+
OPM_SHARE_DATA_DIR: opts.dataDir,
|
|
616
|
+
},
|
|
617
|
+
})
|
|
618
|
+
child.unref()
|
|
619
|
+
|
|
620
|
+
const pidInfo = {
|
|
621
|
+
pid: child.pid,
|
|
622
|
+
url: `http://${opts.host}:${opts.port}`,
|
|
623
|
+
host: opts.host,
|
|
624
|
+
port: opts.port,
|
|
625
|
+
dataDir: opts.dataDir,
|
|
626
|
+
started: new Date().toISOString(),
|
|
627
|
+
}
|
|
628
|
+
fs.writeFileSync(pidFile, JSON.stringify(pidInfo, null, 2), 'utf8')
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
_serverrStop() {
|
|
632
|
+
const pidInfo = readServePid()
|
|
633
|
+
if (!pidInfo) {
|
|
634
|
+
output({ stopped: true, message: 'No server process found (pid file missing)' })
|
|
635
|
+
return
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (!isProcessAlive(pidInfo.pid)) {
|
|
639
|
+
fs.unlinkSync(getServePidFile())
|
|
640
|
+
output({ stopped: true, message: 'Process was not running (cleaned stale pid file)' })
|
|
641
|
+
return
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
try {
|
|
645
|
+
process.kill(pidInfo.pid, 'SIGTERM')
|
|
646
|
+
} catch {}
|
|
647
|
+
|
|
648
|
+
const pidFile = getServePidFile()
|
|
649
|
+
if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile)
|
|
650
|
+
output({ stopped: true, pid: pidInfo.pid, url: pidInfo.url, message: 'Server stopped' })
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
_serverrStatus() {
|
|
654
|
+
const pidInfo = readServePid()
|
|
655
|
+
if (!pidInfo) {
|
|
656
|
+
output({ running: false, message: 'No server process found' })
|
|
657
|
+
return
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (!isProcessAlive(pidInfo.pid)) {
|
|
661
|
+
const pidFile = getServePidFile()
|
|
662
|
+
if (fs.existsSync(pidFile)) fs.unlinkSync(pidFile)
|
|
663
|
+
output({ running: false, pid: pidInfo.pid, message: 'Process was not running (cleaned stale pid file)' })
|
|
664
|
+
return
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
output({ running: true, ...pidInfo })
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
printHelp() {
|
|
671
|
+
const actionLines = Object.entries(ACTIONS)
|
|
672
|
+
.map(([sig, { desc }]) => ` ${sig.padEnd(12)} ${desc}`)
|
|
673
|
+
.join('\n')
|
|
674
|
+
|
|
675
|
+
console.log(`
|
|
676
|
+
Usage: opm share <action> [args] [options]
|
|
677
|
+
|
|
678
|
+
Actions:
|
|
679
|
+
${actionLines}
|
|
680
|
+
|
|
681
|
+
Server subcommands:
|
|
682
|
+
server start Start local server (background)
|
|
683
|
+
server stop Stop local server
|
|
684
|
+
server status Check local server status
|
|
685
|
+
|
|
686
|
+
Shorthand:
|
|
687
|
+
opm share "<content>" [-t title] Upload (without "upload" keyword)
|
|
688
|
+
opm share -f <file> Upload file content (title auto-generated)
|
|
689
|
+
echo "..." | opm share [-t title] Upload from stdin
|
|
690
|
+
|
|
691
|
+
Options:
|
|
692
|
+
-t, --title <title> Title (default: first 30 chars of content)
|
|
693
|
+
-f, --file <path> Read content from file
|
|
694
|
+
-i, --id <id> Custom share id (upload/zip; easier to remember)
|
|
695
|
+
-p, --password <password> Password (default: config share.password, fallback 0000)
|
|
696
|
+
-n, --count <n> Number of items (list, default: 10)
|
|
697
|
+
-d, --dir <dir> Extract zip to this directory (get)
|
|
698
|
+
-h, --help Show this help
|
|
699
|
+
|
|
700
|
+
Server config (config.json5):
|
|
701
|
+
share.password Default password for share operations
|
|
702
|
+
share.server.active Active server (preset key or URL)
|
|
703
|
+
share.serve.host / .port / .dataDir Local serve parameters
|
|
704
|
+
|
|
705
|
+
Examples:
|
|
706
|
+
opm share config Show current share config
|
|
707
|
+
opm share server start Start local server (background)
|
|
708
|
+
opm share server status Check if server is running
|
|
709
|
+
opm share server stop Stop local server
|
|
710
|
+
opm share "hello world" -t test Upload text
|
|
711
|
+
opm share "hello world" -i myid Upload text with custom id "myid"
|
|
712
|
+
opm share "hello world" -p 1234 Upload text with password override
|
|
713
|
+
opm share -f doc.md Upload file (title auto from content)
|
|
714
|
+
opm share -f doc.md -t "README" Upload file with custom title
|
|
715
|
+
echo "hello" | opm share Upload from stdin
|
|
716
|
+
opm share zip ./src Upload folder as zip
|
|
717
|
+
opm share zip ./config.json5 Upload file as zip
|
|
718
|
+
opm share get Get the latest share (JSON)
|
|
719
|
+
opm share get myid Get share by custom id
|
|
720
|
+
opm share get abc12345 Get share by random id
|
|
721
|
+
opm share get abc12345 -p 1234 Get with password override
|
|
722
|
+
opm share get abc12345 -d ./out Download and extract zip to ./out
|
|
723
|
+
opm share view View the latest share content
|
|
724
|
+
opm share view myid View share by custom id
|
|
725
|
+
opm share open Open the latest share in browser
|
|
726
|
+
opm share open myid Open share by custom id in browser
|
|
727
|
+
opm share list List recent 10 shares
|
|
728
|
+
opm share list -n 5 List recent 5 shares
|
|
729
|
+
opm share delete abc12345 Delete a share by id
|
|
730
|
+
opm share clear --days 7 Delete shares older than 7 days
|
|
731
|
+
opm share clear --keep 50 Keep only the most recent 50 shares
|
|
732
|
+
opm share clear --days 7 --keep 50 Both combined
|
|
733
|
+
`)
|
|
734
|
+
}
|
|
735
|
+
}
|