pal-explorer-cli 0.4.11 → 0.4.13
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 +149 -149
- package/bin/pal.js +63 -2
- package/extensions/@palexplorer/analytics/extension.json +20 -1
- package/extensions/@palexplorer/analytics/index.js +19 -9
- package/extensions/@palexplorer/audit/extension.json +14 -0
- package/extensions/@palexplorer/auth-email/extension.json +15 -0
- package/extensions/@palexplorer/auth-oauth/extension.json +15 -0
- package/extensions/@palexplorer/chat/extension.json +14 -0
- package/extensions/@palexplorer/discovery/extension.json +17 -0
- package/extensions/@palexplorer/discovery/index.js +1 -1
- package/extensions/@palexplorer/email-notifications/extension.json +23 -0
- package/extensions/@palexplorer/groups/extension.json +15 -0
- package/extensions/@palexplorer/share-links/extension.json +15 -0
- package/extensions/@palexplorer/sync/extension.json +16 -0
- package/extensions/@palexplorer/user-mgmt/extension.json +15 -0
- package/lib/capabilities.js +24 -24
- package/lib/commands/analytics.js +175 -175
- package/lib/commands/api-keys.js +131 -131
- package/lib/commands/audit.js +235 -235
- package/lib/commands/auth.js +137 -137
- package/lib/commands/backup.js +76 -76
- package/lib/commands/billing.js +148 -148
- package/lib/commands/chat.js +217 -217
- package/lib/commands/cloud-backup.js +231 -231
- package/lib/commands/comment.js +99 -99
- package/lib/commands/completion.js +203 -203
- package/lib/commands/compliance.js +218 -218
- package/lib/commands/config.js +136 -136
- package/lib/commands/connect.js +44 -44
- package/lib/commands/dept.js +294 -294
- package/lib/commands/device.js +146 -146
- package/lib/commands/download.js +240 -226
- package/lib/commands/explorer.js +178 -178
- package/lib/commands/extension.js +1060 -970
- package/lib/commands/favorite.js +90 -90
- package/lib/commands/federation.js +270 -270
- package/lib/commands/file.js +533 -533
- package/lib/commands/group.js +271 -271
- package/lib/commands/gui-share.js +29 -29
- package/lib/commands/init.js +61 -61
- package/lib/commands/invite.js +59 -59
- package/lib/commands/list.js +58 -58
- package/lib/commands/log.js +116 -116
- package/lib/commands/nearby.js +108 -108
- package/lib/commands/network.js +251 -251
- package/lib/commands/notify.js +198 -198
- package/lib/commands/org.js +273 -273
- package/lib/commands/pal.js +403 -180
- package/lib/commands/permissions.js +216 -216
- package/lib/commands/pin.js +97 -97
- package/lib/commands/protocol.js +357 -357
- package/lib/commands/rbac.js +147 -147
- package/lib/commands/recover.js +36 -36
- package/lib/commands/register.js +171 -171
- package/lib/commands/relay.js +131 -131
- package/lib/commands/remote.js +368 -368
- package/lib/commands/revoke.js +50 -50
- package/lib/commands/scanner.js +280 -280
- package/lib/commands/schedule.js +344 -344
- package/lib/commands/scim.js +203 -203
- package/lib/commands/search.js +181 -181
- package/lib/commands/serve.js +438 -438
- package/lib/commands/server.js +350 -350
- package/lib/commands/share-link.js +199 -199
- package/lib/commands/share.js +336 -323
- package/lib/commands/sso.js +200 -200
- package/lib/commands/status.js +145 -145
- package/lib/commands/stream.js +562 -562
- package/lib/commands/su.js +187 -187
- package/lib/commands/sync.js +979 -979
- package/lib/commands/transfers.js +152 -152
- package/lib/commands/uninstall.js +188 -188
- package/lib/commands/update.js +204 -204
- package/lib/commands/user.js +276 -276
- package/lib/commands/vfs.js +84 -84
- package/lib/commands/web-login.js +79 -79
- package/lib/commands/web.js +52 -52
- package/lib/commands/webhook.js +180 -180
- package/lib/commands/whoami.js +59 -59
- package/lib/commands/workspace.js +121 -121
- package/lib/core/billing.js +16 -5
- package/lib/core/dhtDiscovery.js +9 -2
- package/lib/core/discoveryClient.js +13 -7
- package/lib/core/extensions.js +142 -1
- package/lib/core/identity.js +33 -2
- package/lib/core/imageProcessor.js +109 -0
- package/lib/core/imageTorrent.js +167 -0
- package/lib/core/permissions.js +1 -1
- package/lib/core/pro.js +11 -4
- package/lib/core/serverList.js +4 -1
- package/lib/core/shares.js +12 -1
- package/lib/core/signalingServer.js +14 -2
- package/lib/core/su.js +1 -1
- package/lib/core/users.js +1 -1
- package/lib/protocol/messages.js +12 -3
- package/lib/utils/explorer.js +1 -1
- package/lib/utils/help.js +357 -357
- package/lib/utils/torrent.js +1 -0
- package/package.json +4 -3
package/lib/commands/share.js
CHANGED
|
@@ -1,323 +1,336 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import { addShare, storeShareKey } from '../core/shares.js';
|
|
5
|
-
import { getGroup } from '../core/groups.js';
|
|
6
|
-
import config from '../utils/config.js';
|
|
7
|
-
import logger from '../utils/logger.js';
|
|
8
|
-
import { requireRole, getFriends } from '../core/users.js';
|
|
9
|
-
import { checkLimit } from '../core/pro.js';
|
|
10
|
-
|
|
11
|
-
export default function shareCommand(program) {
|
|
12
|
-
program
|
|
13
|
-
.command('share-rename <id> <name>')
|
|
14
|
-
.description('rename a shared resource')
|
|
15
|
-
.action((id, name) => {
|
|
16
|
-
if (!name || !name.trim()) {
|
|
17
|
-
console.log(chalk.red('Error: name cannot be empty.'));
|
|
18
|
-
process.exitCode = 1;
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
const shares = config.get('shares') || [];
|
|
22
|
-
const share = shares.find(s => s.id === id);
|
|
23
|
-
if (!share) {
|
|
24
|
-
console.log(chalk.red('Share not found.'));
|
|
25
|
-
process.exitCode = 1;
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
share.name = name;
|
|
29
|
-
config.set('shares', shares);
|
|
30
|
-
console.log(chalk.green(`✔ Share renamed to: ${name}`));
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
program
|
|
34
|
-
.command('share <file>')
|
|
35
|
-
.description('share a file, folder, or drive via P2P')
|
|
36
|
-
.option('-v, --visibility <type>', 'Visibility: global, private, group, network, link-only', 'global')
|
|
37
|
-
.option('-w, --with <pals...>', 'Specific pals to share with (names or IDs)')
|
|
38
|
-
.option('--with-group <group>', 'Share with all members of a group')
|
|
39
|
-
.option('--with-network <network>', 'Share with a network')
|
|
40
|
-
.option('--streamable', 'Enable media streaming (audio/video playback without download)')
|
|
41
|
-
.option('--no-recursive', 'Share only top-level files in folder (no subfolders)')
|
|
42
|
-
.option('--expires <duration>', 'Auto-expire after duration (e.g. 1h, 3d, 7d, 30d)')
|
|
43
|
-
.option('--max-downloads <n>', 'Auto-expire after N downloads (0 = unlimited)', '0')
|
|
44
|
-
.option('--password <pwd>', 'Password-protect this share')
|
|
45
|
-
.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
$
|
|
53
|
-
$
|
|
54
|
-
$
|
|
55
|
-
$
|
|
56
|
-
$
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
console.log(chalk.red(
|
|
88
|
-
process.exitCode = 1;
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (group
|
|
103
|
-
console.log(chalk.
|
|
104
|
-
process.exitCode = 1;
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
//
|
|
197
|
-
if (options.
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { addShare, storeShareKey } from '../core/shares.js';
|
|
5
|
+
import { getGroup } from '../core/groups.js';
|
|
6
|
+
import config from '../utils/config.js';
|
|
7
|
+
import logger from '../utils/logger.js';
|
|
8
|
+
import { requireRole, getFriends } from '../core/users.js';
|
|
9
|
+
import { checkLimit } from '../core/pro.js';
|
|
10
|
+
|
|
11
|
+
export default function shareCommand(program) {
|
|
12
|
+
program
|
|
13
|
+
.command('share-rename <id> <name>')
|
|
14
|
+
.description('rename a shared resource')
|
|
15
|
+
.action((id, name) => {
|
|
16
|
+
if (!name || !name.trim()) {
|
|
17
|
+
console.log(chalk.red('Error: name cannot be empty.'));
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const shares = config.get('shares') || [];
|
|
22
|
+
const share = shares.find(s => s.id === id);
|
|
23
|
+
if (!share) {
|
|
24
|
+
console.log(chalk.red('Share not found.'));
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
share.name = name;
|
|
29
|
+
config.set('shares', shares);
|
|
30
|
+
console.log(chalk.green(`✔ Share renamed to: ${name}`));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
program
|
|
34
|
+
.command('share <file>')
|
|
35
|
+
.description('share a file, folder, or drive via P2P')
|
|
36
|
+
.option('-v, --visibility <type>', 'Visibility: global, private, group, network, link-only', 'global')
|
|
37
|
+
.option('-w, --with <pals...>', 'Specific pals to share with (names or IDs)')
|
|
38
|
+
.option('--with-group <group>', 'Share with all members of a group')
|
|
39
|
+
.option('--with-network <network>', 'Share with a network')
|
|
40
|
+
.option('--streamable', 'Enable media streaming (audio/video playback without download)')
|
|
41
|
+
.option('--no-recursive', 'Share only top-level files in folder (no subfolders)')
|
|
42
|
+
.option('--expires <duration>', 'Auto-expire after duration (e.g. 1h, 3d, 7d, 30d)')
|
|
43
|
+
.option('--max-downloads <n>', 'Auto-expire after N downloads (0 = unlimited)', '0')
|
|
44
|
+
.option('--password <pwd>', 'Password-protect this share')
|
|
45
|
+
.option('--category <type>', 'Category: documents, media, projects, games, backups, other')
|
|
46
|
+
.option('--description <text>', 'Share description (max 200 chars)')
|
|
47
|
+
.option('--color <hex>', 'Share color (hex, e.g. #89b4fa)')
|
|
48
|
+
.option('--icon <emoji>', 'Share icon emoji')
|
|
49
|
+
.option('--tags <list>', 'Comma-separated tags, e.g. "photos,vacation,2026"')
|
|
50
|
+
.addHelpText('after', `
|
|
51
|
+
Examples:
|
|
52
|
+
$ pal share ./photos Share a folder publicly (recursive)
|
|
53
|
+
$ pal share ./photos --no-recursive Share only top-level files in folder
|
|
54
|
+
$ pal share ./docs -v private -w alice Share privately with pal "alice"
|
|
55
|
+
$ pal share ./project --with-group team Share with entire group
|
|
56
|
+
$ pal share ./file.zip --password secret123 Password-protected share
|
|
57
|
+
$ pal share ./file.zip Share a single file
|
|
58
|
+
$ pal share ./music --streamable Share as streaming media server
|
|
59
|
+
$ pal share ./report.pdf --expires 3d Share for 3 days then auto-expire
|
|
60
|
+
$ pal share ./file.zip --max-downloads 5 Expire after 5 downloads
|
|
61
|
+
$ pal share ./docs -v network --with-network mynet
|
|
62
|
+
|
|
63
|
+
Note: Private shares create an encrypted copy of your files for E2E seeding.
|
|
64
|
+
This uses additional disk space equal to the original share size.
|
|
65
|
+
`)
|
|
66
|
+
.action(async (filePath, options) => {
|
|
67
|
+
if (!filePath || !filePath.trim()) {
|
|
68
|
+
console.log(chalk.red('Error: file path cannot be empty.'));
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const VALID_VISIBILITIES = ['global', 'private', 'group', 'network', 'link-only', 'public'];
|
|
73
|
+
if (!VALID_VISIBILITIES.includes(options.visibility)) {
|
|
74
|
+
console.log(chalk.red(`Invalid visibility '${options.visibility}'. Must be one of: ${VALID_VISIBILITIES.join(', ')}`));
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const maxDl = parseInt(options.maxDownloads);
|
|
79
|
+
if (isNaN(maxDl) || maxDl < 0) {
|
|
80
|
+
console.log(chalk.red('--max-downloads must be a non-negative integer.'));
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const absolutePath = path.resolve(filePath);
|
|
85
|
+
try {
|
|
86
|
+
try { requireRole('user'); } catch (e) {
|
|
87
|
+
console.log(chalk.red(e.message));
|
|
88
|
+
process.exitCode = 1;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!fs.existsSync(absolutePath)) {
|
|
92
|
+
console.log(chalk.red(`Path does not exist: ${absolutePath}`));
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const friends = getFriends();
|
|
97
|
+
|
|
98
|
+
// Validate pals if provided
|
|
99
|
+
let recipients = [];
|
|
100
|
+
if (options.withGroup) {
|
|
101
|
+
const group = getGroup(options.withGroup);
|
|
102
|
+
if (!group) {
|
|
103
|
+
console.log(chalk.red(`Group '${options.withGroup}' not found.`));
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (group.members.length === 0) {
|
|
108
|
+
console.log(chalk.yellow(`Group '${group.name}' has no members.`));
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
recipients = group.members.map(m => {
|
|
113
|
+
const pal = friends.find(f => f.id === m.id);
|
|
114
|
+
return pal || { name: m.name, id: m.id, handle: m.handle };
|
|
115
|
+
});
|
|
116
|
+
console.log(chalk.cyan(`Sharing with group '${group.name}' (${recipients.length} members)`));
|
|
117
|
+
}
|
|
118
|
+
if (options.with) {
|
|
119
|
+
const palRecipients = options.with.map(nameOrId => {
|
|
120
|
+
const stripped = nameOrId.replace(/^@/, '');
|
|
121
|
+
const pal = friends.find(f =>
|
|
122
|
+
f.id === stripped || f.name === stripped || f.handle === stripped || f.publicKey === stripped
|
|
123
|
+
);
|
|
124
|
+
if (!pal) {
|
|
125
|
+
console.warn(chalk.yellow(`Warning: Pal '${stripped}' not found in your list.`));
|
|
126
|
+
return { name: stripped, id: 'unknown' };
|
|
127
|
+
}
|
|
128
|
+
return pal;
|
|
129
|
+
});
|
|
130
|
+
// Merge, avoid duplicates by id
|
|
131
|
+
for (const r of palRecipients) {
|
|
132
|
+
if (!recipients.find(e => e.id === r.id)) {
|
|
133
|
+
recipients.push(r);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
checkLimit('maxShareRecipients', recipients.length);
|
|
139
|
+
|
|
140
|
+
console.log(chalk.blue(`Preparing to share: ${absolutePath}`));
|
|
141
|
+
|
|
142
|
+
let type = 'file';
|
|
143
|
+
try {
|
|
144
|
+
const stat = fs.statSync(absolutePath);
|
|
145
|
+
if (stat.isDirectory()) {
|
|
146
|
+
const parsed = path.parse(absolutePath);
|
|
147
|
+
type = parsed.dir === parsed.root || absolutePath === parsed.root ? 'drive' : 'folder';
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
// path doesn't exist yet — default to 'file'
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const recursive = options.recursive !== false;
|
|
154
|
+
let expiresIn = 0;
|
|
155
|
+
if (options.expires) {
|
|
156
|
+
expiresIn = parseDuration(options.expires);
|
|
157
|
+
if (!expiresIn) {
|
|
158
|
+
console.log(chalk.red('Invalid --expires format. Use: 1h, 3d, 7d, 30d'));
|
|
159
|
+
process.exitCode = 1;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const maxDownloads = parseInt(options.maxDownloads) || 0;
|
|
164
|
+
const password = options.password || null;
|
|
165
|
+
const category = options.category || null;
|
|
166
|
+
const description = options.description ? options.description.slice(0, 200) : null;
|
|
167
|
+
const color = options.color || null;
|
|
168
|
+
const icon = options.icon || null;
|
|
169
|
+
const share = addShare(absolutePath, type, options.visibility, { read: true }, recipients, { recursive, expiresIn, maxDownloads, password, category, description, color, icon });
|
|
170
|
+
if (options.tags) {
|
|
171
|
+
const { updateShare } = await import('../core/shares.js');
|
|
172
|
+
updateShare(share.id, { tags: options.tags.split(',').map(t => t.trim()).filter(Boolean) });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Set streamable flag
|
|
176
|
+
if (options.streamable) {
|
|
177
|
+
const shares = config.get('shares');
|
|
178
|
+
const idx = shares.findIndex(s => s.id === share.id);
|
|
179
|
+
if (idx !== -1) {
|
|
180
|
+
shares[idx].streamable = true;
|
|
181
|
+
config.set('shares', shares);
|
|
182
|
+
}
|
|
183
|
+
console.log(chalk.magenta(' Media streaming enabled for this share'));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Set network association
|
|
187
|
+
if (options.withNetwork) {
|
|
188
|
+
const shares = config.get('shares');
|
|
189
|
+
const idx = shares.findIndex(s => s.id === share.id);
|
|
190
|
+
if (idx !== -1) {
|
|
191
|
+
shares[idx].sharedWithNetworks = [options.withNetwork];
|
|
192
|
+
config.set('shares', shares);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Persist group association
|
|
197
|
+
if (options.withGroup) {
|
|
198
|
+
const group = getGroup(options.withGroup);
|
|
199
|
+
if (group) {
|
|
200
|
+
const shares = config.get('shares');
|
|
201
|
+
const idx = shares.findIndex(s => s.id === share.id);
|
|
202
|
+
if (idx !== -1) {
|
|
203
|
+
shares[idx].sharedWithGroups = [group.id];
|
|
204
|
+
config.set('shares', shares);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Encrypt for private shares that have named recipients
|
|
210
|
+
if (options.visibility === 'private' && share.recipients.length > 0) {
|
|
211
|
+
const { generateShareKey, encryptDirectory, getEncryptedShareDir, encryptShareKeyForRecipient } = await import('../crypto/shareEncryption.js');
|
|
212
|
+
const shareKey = generateShareKey();
|
|
213
|
+
const encDir = getEncryptedShareDir(share.id);
|
|
214
|
+
console.log(chalk.gray('Encrypting files for private share...'));
|
|
215
|
+
const encStart = Date.now();
|
|
216
|
+
const encSpinner = setInterval(() => {
|
|
217
|
+
const elapsed = ((Date.now() - encStart) / 1000).toFixed(0);
|
|
218
|
+
process.stdout.write(`\r${chalk.gray(` Encrypting... ${elapsed}s`)}`);
|
|
219
|
+
}, 1000);
|
|
220
|
+
try {
|
|
221
|
+
encryptDirectory(absolutePath, encDir, shareKey);
|
|
222
|
+
} finally {
|
|
223
|
+
clearInterval(encSpinner);
|
|
224
|
+
process.stdout.write('\r' + ' '.repeat(40) + '\r');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Wrap the shareKey for each recipient
|
|
228
|
+
const encryptedShareKeys = {};
|
|
229
|
+
for (const recipient of share.recipients) {
|
|
230
|
+
const pal = friends.find(f => f.name === recipient.name || f.id === recipient.id || f.handle === recipient.handle);
|
|
231
|
+
if (pal?.id && pal.id !== 'unknown') {
|
|
232
|
+
encryptedShareKeys[pal.id] = encryptShareKeyForRecipient(shareKey, pal.id);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Store share key securely in credential store
|
|
237
|
+
await storeShareKey(share.id, shareKey.toString('hex'));
|
|
238
|
+
|
|
239
|
+
// Persist encryption metadata onto the stored share (without shareKeyHex)
|
|
240
|
+
const shares = config.get('shares');
|
|
241
|
+
const idx = shares.findIndex(s => s.id === share.id);
|
|
242
|
+
if (idx !== -1) {
|
|
243
|
+
shares[idx].encryptedPath = encDir;
|
|
244
|
+
shares[idx].encryptedShareKeys = encryptedShareKeys;
|
|
245
|
+
config.set('shares', shares);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log(chalk.green('Encryption complete.'));
|
|
249
|
+
|
|
250
|
+
// Warn about disk overhead for private shares
|
|
251
|
+
try {
|
|
252
|
+
const stat = fs.statSync(absolutePath);
|
|
253
|
+
if (stat.isDirectory()) {
|
|
254
|
+
let totalSize = 0;
|
|
255
|
+
const walk = (dir) => {
|
|
256
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
257
|
+
const full = path.join(dir, entry.name);
|
|
258
|
+
if (entry.isFile()) totalSize += fs.statSync(full).size;
|
|
259
|
+
else if (entry.isDirectory()) walk(full);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
walk(absolutePath);
|
|
263
|
+
const sizeMB = (totalSize / 1024 / 1024).toFixed(0);
|
|
264
|
+
if (totalSize > 1024 * 1024 * 1024) {
|
|
265
|
+
const sizeGB = (totalSize / 1024 / 1024 / 1024).toFixed(1);
|
|
266
|
+
console.log(chalk.yellow(` Warning: Private shares create an encrypted copy (~${sizeGB} GB additional disk space).`));
|
|
267
|
+
console.log(chalk.yellow(` Total disk usage for this share: ~${(totalSize * 2 / 1024 / 1024 / 1024).toFixed(1)} GB (original + encrypted copy).`));
|
|
268
|
+
} else if (totalSize > 50 * 1024 * 1024) {
|
|
269
|
+
console.log(chalk.yellow(` Note: Private shares create an encrypted copy (~${sizeMB} MB additional disk space).`));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} catch {}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
logger.info(`Share added: ${absolutePath}`, { visibility: options.visibility, type, id: share.id });
|
|
276
|
+
console.log(chalk.green(`✔ Resource added to ${options.visibility} share list!`));
|
|
277
|
+
if (recipients.length > 0) {
|
|
278
|
+
console.log(chalk.cyan(` Shared with: ${recipients.map(r => r.name).join(', ')}`));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Send PAL/1.0 share.offer envelopes to recipients
|
|
282
|
+
if (options.visibility === 'private' && recipients.length > 0) {
|
|
283
|
+
try {
|
|
284
|
+
const { getIdentity } = await import('../core/identity.js');
|
|
285
|
+
const identity = await getIdentity();
|
|
286
|
+
if (identity?.privateKey) {
|
|
287
|
+
const { initiateShare } = await import('../protocol/negotiation.js');
|
|
288
|
+
const { postTo, getPrimaryServer } = await import('../core/discoveryClient.js');
|
|
289
|
+
const keyPair = {
|
|
290
|
+
publicKey: Buffer.from(identity.publicKey, 'hex'),
|
|
291
|
+
privateKey: Buffer.from(identity.privateKey, 'hex'),
|
|
292
|
+
};
|
|
293
|
+
let sent = 0;
|
|
294
|
+
for (const r of recipients) {
|
|
295
|
+
const pal = friends.find(f => f.id === r.id);
|
|
296
|
+
if (pal?.id && pal.id !== 'unknown') {
|
|
297
|
+
const result = await initiateShare(keyPair, pal.id, share, options.policy || {});
|
|
298
|
+
if (result.ok) {
|
|
299
|
+
await postTo(getPrimaryServer(), '/api/v1/messages', {
|
|
300
|
+
toHandle: pal.handle || pal.name,
|
|
301
|
+
fromHandle: identity.handle || identity.name,
|
|
302
|
+
payload: result.envelope,
|
|
303
|
+
});
|
|
304
|
+
sent++;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (sent > 0) console.log(chalk.gray(` PAL/1.0 share offers sent to ${sent} recipient(s)`));
|
|
309
|
+
}
|
|
310
|
+
} catch {
|
|
311
|
+
// Protocol notifications are best-effort
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (share.password) console.log(chalk.yellow(' Password protected'));
|
|
316
|
+
if (share.expiresAt) console.log(chalk.gray(` Expires: ${new Date(share.expiresAt).toLocaleString()}`));
|
|
317
|
+
if (share.maxDownloads) console.log(chalk.gray(` Max downloads: ${share.maxDownloads}`));
|
|
318
|
+
console.log(chalk.white('To start seeding, use: pal serve'));
|
|
319
|
+
console.log(chalk.gray(`ID: ${share.id}`));
|
|
320
|
+
} catch (err) {
|
|
321
|
+
logger.error(`Share failed: ${err.message}`, { path: absolutePath });
|
|
322
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function parseDuration(str) {
|
|
328
|
+
const match = str.match(/^(\d+)([dhm])$/);
|
|
329
|
+
if (!match) return null;
|
|
330
|
+
const n = parseInt(match[1]);
|
|
331
|
+
const unit = match[2];
|
|
332
|
+
if (unit === 'd') return n * 86400000;
|
|
333
|
+
if (unit === 'h') return n * 3600000;
|
|
334
|
+
if (unit === 'm') return n * 60000;
|
|
335
|
+
return null;
|
|
336
|
+
}
|