pal-explorer-cli 0.4.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.
Files changed (156) hide show
  1. package/LICENSE.md +18 -0
  2. package/README.md +314 -0
  3. package/bin/pal.js +230 -0
  4. package/extensions/@palexplorer/analytics/README.md +45 -0
  5. package/extensions/@palexplorer/analytics/docs/MONETIZATION.md +14 -0
  6. package/extensions/@palexplorer/analytics/docs/PLAN.md +23 -0
  7. package/extensions/@palexplorer/analytics/docs/PRIVACY.md +38 -0
  8. package/extensions/@palexplorer/analytics/extension.json +27 -0
  9. package/extensions/@palexplorer/analytics/index.js +186 -0
  10. package/extensions/@palexplorer/analytics/test/analytics.test.js +82 -0
  11. package/extensions/@palexplorer/audit/extension.json +17 -0
  12. package/extensions/@palexplorer/audit/index.js +2 -0
  13. package/extensions/@palexplorer/auth-email/extension.json +17 -0
  14. package/extensions/@palexplorer/auth-email/index.js +102 -0
  15. package/extensions/@palexplorer/auth-oauth/extension.json +16 -0
  16. package/extensions/@palexplorer/auth-oauth/index.js +199 -0
  17. package/extensions/@palexplorer/chat/extension.json +17 -0
  18. package/extensions/@palexplorer/chat/index.js +2 -0
  19. package/extensions/@palexplorer/discovery/extension.json +16 -0
  20. package/extensions/@palexplorer/discovery/index.js +111 -0
  21. package/extensions/@palexplorer/email-notifications/extension.json +23 -0
  22. package/extensions/@palexplorer/email-notifications/index.js +242 -0
  23. package/extensions/@palexplorer/explorer-integration/extension.json +13 -0
  24. package/extensions/@palexplorer/explorer-integration/index.js +122 -0
  25. package/extensions/@palexplorer/groups/extension.json +17 -0
  26. package/extensions/@palexplorer/groups/index.js +2 -0
  27. package/extensions/@palexplorer/networks/extension.json +17 -0
  28. package/extensions/@palexplorer/networks/index.js +2 -0
  29. package/extensions/@palexplorer/share-links/extension.json +17 -0
  30. package/extensions/@palexplorer/share-links/index.js +2 -0
  31. package/extensions/@palexplorer/sync/extension.json +17 -0
  32. package/extensions/@palexplorer/sync/index.js +2 -0
  33. package/extensions/@palexplorer/user-mgmt/extension.json +17 -0
  34. package/extensions/@palexplorer/user-mgmt/index.js +2 -0
  35. package/extensions/@palexplorer/vfs/extension.json +17 -0
  36. package/extensions/@palexplorer/vfs/index.js +167 -0
  37. package/lib/capabilities.js +263 -0
  38. package/lib/commands/analytics.js +175 -0
  39. package/lib/commands/api-keys.js +131 -0
  40. package/lib/commands/audit.js +235 -0
  41. package/lib/commands/auth.js +137 -0
  42. package/lib/commands/backup.js +76 -0
  43. package/lib/commands/billing.js +148 -0
  44. package/lib/commands/chat.js +217 -0
  45. package/lib/commands/cloud-backup.js +231 -0
  46. package/lib/commands/comment.js +99 -0
  47. package/lib/commands/completion.js +203 -0
  48. package/lib/commands/compliance.js +218 -0
  49. package/lib/commands/config.js +136 -0
  50. package/lib/commands/connect.js +44 -0
  51. package/lib/commands/dept.js +294 -0
  52. package/lib/commands/device.js +146 -0
  53. package/lib/commands/download.js +226 -0
  54. package/lib/commands/explorer.js +178 -0
  55. package/lib/commands/extension.js +970 -0
  56. package/lib/commands/favorite.js +90 -0
  57. package/lib/commands/federation.js +270 -0
  58. package/lib/commands/file.js +533 -0
  59. package/lib/commands/group.js +271 -0
  60. package/lib/commands/gui-share.js +29 -0
  61. package/lib/commands/init.js +61 -0
  62. package/lib/commands/invite.js +59 -0
  63. package/lib/commands/list.js +59 -0
  64. package/lib/commands/log.js +116 -0
  65. package/lib/commands/nearby.js +108 -0
  66. package/lib/commands/network.js +251 -0
  67. package/lib/commands/notify.js +198 -0
  68. package/lib/commands/org.js +273 -0
  69. package/lib/commands/pal.js +180 -0
  70. package/lib/commands/permissions.js +216 -0
  71. package/lib/commands/pin.js +97 -0
  72. package/lib/commands/protocol.js +357 -0
  73. package/lib/commands/rbac.js +147 -0
  74. package/lib/commands/recover.js +36 -0
  75. package/lib/commands/register.js +171 -0
  76. package/lib/commands/relay.js +131 -0
  77. package/lib/commands/remote.js +368 -0
  78. package/lib/commands/revoke.js +50 -0
  79. package/lib/commands/scanner.js +280 -0
  80. package/lib/commands/schedule.js +344 -0
  81. package/lib/commands/scim.js +203 -0
  82. package/lib/commands/search.js +181 -0
  83. package/lib/commands/serve.js +438 -0
  84. package/lib/commands/server.js +350 -0
  85. package/lib/commands/share-link.js +199 -0
  86. package/lib/commands/share.js +323 -0
  87. package/lib/commands/sso.js +200 -0
  88. package/lib/commands/status.js +136 -0
  89. package/lib/commands/stream.js +562 -0
  90. package/lib/commands/su.js +187 -0
  91. package/lib/commands/sync.js +827 -0
  92. package/lib/commands/transfers.js +152 -0
  93. package/lib/commands/uninstall.js +188 -0
  94. package/lib/commands/update.js +204 -0
  95. package/lib/commands/user.js +276 -0
  96. package/lib/commands/vfs.js +84 -0
  97. package/lib/commands/web.js +52 -0
  98. package/lib/commands/webhook.js +180 -0
  99. package/lib/commands/whoami.js +59 -0
  100. package/lib/commands/workspace.js +121 -0
  101. package/lib/core/accessLog.js +54 -0
  102. package/lib/core/analytics.js +99 -0
  103. package/lib/core/backup.js +84 -0
  104. package/lib/core/billing.js +336 -0
  105. package/lib/core/bitfieldStore.js +53 -0
  106. package/lib/core/connectionManager.js +182 -0
  107. package/lib/core/dhtDiscovery.js +148 -0
  108. package/lib/core/discoveryClient.js +408 -0
  109. package/lib/core/extensionAnalyzer.js +357 -0
  110. package/lib/core/extensionSandbox.js +250 -0
  111. package/lib/core/extensionWorkerHost.js +166 -0
  112. package/lib/core/extensions.js +1082 -0
  113. package/lib/core/fileDiff.js +69 -0
  114. package/lib/core/groups.js +119 -0
  115. package/lib/core/identity.js +340 -0
  116. package/lib/core/mdnsService.js +126 -0
  117. package/lib/core/networks.js +81 -0
  118. package/lib/core/permissions.js +109 -0
  119. package/lib/core/pro.js +27 -0
  120. package/lib/core/resolver.js +74 -0
  121. package/lib/core/serverList.js +224 -0
  122. package/lib/core/sharePolicy.js +69 -0
  123. package/lib/core/shares.js +325 -0
  124. package/lib/core/signalingServer.js +441 -0
  125. package/lib/core/streamTransport.js +106 -0
  126. package/lib/core/su.js +55 -0
  127. package/lib/core/syncEngine.js +264 -0
  128. package/lib/core/syncState.js +159 -0
  129. package/lib/core/transfers.js +259 -0
  130. package/lib/core/users.js +225 -0
  131. package/lib/core/vfs.js +216 -0
  132. package/lib/core/webServer.js +702 -0
  133. package/lib/core/webrtcStream.js +396 -0
  134. package/lib/crypto/chatEncryption.js +57 -0
  135. package/lib/crypto/shareEncryption.js +195 -0
  136. package/lib/crypto/sharePassword.js +35 -0
  137. package/lib/crypto/streamEncryption.js +189 -0
  138. package/lib/package.json +1 -0
  139. package/lib/protocol/envelope.js +271 -0
  140. package/lib/protocol/handler.js +191 -0
  141. package/lib/protocol/index.js +27 -0
  142. package/lib/protocol/messages.js +247 -0
  143. package/lib/protocol/negotiation.js +127 -0
  144. package/lib/protocol/policy.js +142 -0
  145. package/lib/protocol/router.js +86 -0
  146. package/lib/protocol/sync.js +122 -0
  147. package/lib/utils/cli.js +15 -0
  148. package/lib/utils/config.js +123 -0
  149. package/lib/utils/configIntegrity.js +87 -0
  150. package/lib/utils/downloadDir.js +9 -0
  151. package/lib/utils/explorer.js +83 -0
  152. package/lib/utils/format.js +12 -0
  153. package/lib/utils/help.js +357 -0
  154. package/lib/utils/logger.js +103 -0
  155. package/lib/utils/torrent.js +203 -0
  156. package/package.json +71 -0
@@ -0,0 +1,122 @@
1
+ import crypto from 'crypto';
2
+ import fs from 'fs';
3
+ import { syncManifest, syncDiff, syncRequest, syncConflict } from './messages.js';
4
+ import { isPro } from '../core/pro.js';
5
+
6
+ const DEFAULT_BLOCK_SIZE = 4096;
7
+
8
+ export function buildProtocolManifest(entries, syncPairId, generation) {
9
+ const sorted = [...entries].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
10
+ const checksumInput = sorted.map(e => `${e.relativePath}:${e.hash}:${e.size}`).join('\n');
11
+ const checksum = crypto.createHash('sha256').update(checksumInput).digest('hex');
12
+
13
+ return {
14
+ syncPairId,
15
+ generation,
16
+ entries: sorted.map(e => ({
17
+ path: e.relativePath,
18
+ size: e.size,
19
+ hash: e.hash,
20
+ mtime: e.mtime,
21
+ deleted: false,
22
+ })),
23
+ checksum,
24
+ };
25
+ }
26
+
27
+ export function createManifestEnvelope(keyPair, peerPK, manifest) {
28
+ return syncManifest(keyPair, peerPK, manifest);
29
+ }
30
+
31
+ export function computeDelta(localEntries, remoteEntries) {
32
+ const remoteMap = new Map(remoteEntries.map(e => [e.path, e]));
33
+ const localMap = new Map(localEntries.map(e => [e.path, e]));
34
+
35
+ const added = [];
36
+ const modified = [];
37
+ const deleted = [];
38
+ const unchanged = [];
39
+
40
+ for (const entry of remoteEntries) {
41
+ const local = localMap.get(entry.path);
42
+ if (!local) {
43
+ added.push(entry);
44
+ } else if (local.hash !== entry.hash) {
45
+ modified.push({ path: entry.path, local, remote: entry });
46
+ } else {
47
+ unchanged.push(entry);
48
+ }
49
+ }
50
+
51
+ for (const entry of localEntries) {
52
+ if (!remoteMap.has(entry.path)) {
53
+ deleted.push(entry);
54
+ }
55
+ }
56
+
57
+ return { added, modified, deleted, unchanged };
58
+ }
59
+
60
+ // Delta sync — block-level (Pro only)
61
+ export function computeBlockHashes(filePath, blockSize = DEFAULT_BLOCK_SIZE) {
62
+ if (!isPro()) throw new Error('Delta sync requires Pro.');
63
+
64
+ const fd = fs.openSync(filePath, 'r');
65
+ const stat = fs.fstatSync(fd);
66
+ const hashes = [];
67
+ const buf = Buffer.alloc(blockSize);
68
+
69
+ let offset = 0;
70
+ while (offset < stat.size) {
71
+ const bytesRead = fs.readSync(fd, buf, 0, blockSize, offset);
72
+ const hash = crypto.createHash('sha256').update(buf.subarray(0, bytesRead)).digest('hex');
73
+ hashes.push(hash);
74
+ offset += bytesRead;
75
+ }
76
+
77
+ fs.closeSync(fd);
78
+ return { hashes, blockSize, totalSize: stat.size };
79
+ }
80
+
81
+ export function findChangedBlocks(localHashes, remoteHashes) {
82
+ const changed = [];
83
+ const maxLen = Math.max(localHashes.length, remoteHashes.length);
84
+ for (let i = 0; i < maxLen; i++) {
85
+ if (localHashes[i] !== remoteHashes[i]) {
86
+ changed.push(i);
87
+ }
88
+ }
89
+ return changed;
90
+ }
91
+
92
+ export function createSyncRequest(keyPair, peerPK, syncPairId, files) {
93
+ const requestFiles = files.map(f => {
94
+ if (isPro() && f.mode === 'delta') {
95
+ return {
96
+ path: f.path,
97
+ mode: 'delta',
98
+ blockSize: f.blockSize || DEFAULT_BLOCK_SIZE,
99
+ localBlockHashes: f.localBlockHashes,
100
+ localSize: f.localSize,
101
+ };
102
+ }
103
+ return { path: f.path, mode: 'full' };
104
+ });
105
+
106
+ return syncRequest(keyPair, peerPK, { syncPairId, files: requestFiles });
107
+ }
108
+
109
+ export function createConflictNotification(keyPair, peerPK, syncPairId, conflicts) {
110
+ return syncConflict(keyPair, peerPK, {
111
+ syncPairId,
112
+ conflicts: conflicts.map(c => ({
113
+ path: c.path,
114
+ localHash: c.localHash,
115
+ remoteHash: c.remoteHash,
116
+ baseHash: c.baseHash || null,
117
+ localMtime: c.localMtime,
118
+ remoteMtime: c.remoteMtime,
119
+ resolution: c.resolution || 'keep_both',
120
+ })),
121
+ });
122
+ }
@@ -0,0 +1,15 @@
1
+ import chalk from 'chalk';
2
+
3
+ export function printJson(data) {
4
+ console.log(JSON.stringify(data, null, 2));
5
+ }
6
+
7
+ export function parseCommaList(str) {
8
+ if (!str) return [];
9
+ return str.split(',').map(s => s.trim()).filter(Boolean);
10
+ }
11
+
12
+ export function exitError(message) {
13
+ console.error(chalk.red(message));
14
+ process.exitCode = 1;
15
+ }
@@ -0,0 +1,123 @@
1
+ import Conf from 'conf';
2
+ import keytar from 'keytar';
3
+ import crypto from 'crypto';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+
7
+ const KEYTAR_SERVICE = 'pal-explorer-config';
8
+ const KEYTAR_ACCOUNT = 'encryption-key';
9
+
10
+ let encryptionKey;
11
+ try {
12
+ if (process.platform === 'win32' || process.platform === 'darwin') {
13
+ encryptionKey = await keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);
14
+ if (!encryptionKey) {
15
+ encryptionKey = crypto.randomBytes(32).toString('hex');
16
+ await keytar.setPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT, encryptionKey);
17
+ }
18
+ } else {
19
+ // LOW-1 fix: on Linux, attempt libsecret via secret-tool
20
+ try {
21
+ const { execSync } = await import('child_process');
22
+ encryptionKey = execSync(
23
+ 'secret-tool lookup service pal-explorer-config key encryption-key 2>/dev/null',
24
+ { encoding: 'utf8', timeout: 3000 }
25
+ ).trim();
26
+ if (!encryptionKey) {
27
+ encryptionKey = crypto.randomBytes(32).toString('hex');
28
+ execSync(
29
+ `echo -n "${encryptionKey}" | secret-tool store --label="palexplorer config key" service pal-explorer-config key encryption-key`,
30
+ { timeout: 3000 }
31
+ );
32
+ }
33
+ } catch {
34
+ encryptionKey = null;
35
+ }
36
+ }
37
+ } catch {
38
+ encryptionKey = null;
39
+ }
40
+ const isHeadlessLinux = process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY;
41
+ if (!encryptionKey && !isHeadlessLinux) {
42
+ console.warn('\x1b[33m[SECURITY WARNING] keytar unavailable — config will NOT be encrypted at rest.\x1b[0m');
43
+ console.warn('\x1b[33mPrivate keys are still protected via OS credential store.\x1b[0m');
44
+ }
45
+
46
+ const defaults = {
47
+ identity: null,
48
+ device: null,
49
+ friends: [],
50
+ shares: [],
51
+ settings: {
52
+ downloadDir: '',
53
+ port: 0,
54
+ dht: true,
55
+ maxConnections: 55,
56
+ discovery_servers: [],
57
+ bootstrapNodes: [],
58
+ stunServers: ['stun:stun.l.google.com:19302'],
59
+ turn_server: 'turn.palexplorer.com',
60
+ logLevel: 'info',
61
+ streamTransport: 'p2p',
62
+ customStunServers: [],
63
+ customTurnServers: [],
64
+ tunnelUrl: ''
65
+ }
66
+ };
67
+
68
+ // Migrate plain JSON config → encrypted if needed
69
+ if (encryptionKey) {
70
+ try {
71
+ const tmpConf = new Conf({ projectName: 'pal-explorer-cli', defaults });
72
+ const configPath = tmpConf.path;
73
+ if (fs.existsSync(configPath)) {
74
+ const raw = fs.readFileSync(configPath, 'utf8');
75
+ // If file starts with '{', it's plain JSON — needs migration
76
+ if (raw.trimStart().startsWith('{')) {
77
+ const plainData = JSON.parse(raw);
78
+ // Create encrypted store (overwrites same file)
79
+ const encConf = new Conf({ projectName: 'pal-explorer-cli', defaults, encryptionKey });
80
+ for (const [key, value] of Object.entries(plainData)) {
81
+ encConf.set(key, value);
82
+ }
83
+ }
84
+ }
85
+ } catch {
86
+ // Migration failed — encrypted store will start fresh
87
+ }
88
+ }
89
+
90
+ let config;
91
+ try {
92
+ config = new Conf({
93
+ projectName: 'pal-explorer-cli',
94
+ defaults,
95
+ ...(encryptionKey ? { encryptionKey } : {}),
96
+ });
97
+ // Trigger a read to surface decrypt/parse errors early
98
+ config.store;
99
+ } catch (err) {
100
+ // Config file is encrypted with a different/missing key, or corrupt.
101
+ // Delete the unreadable file and start fresh.
102
+ try {
103
+ const tmpConf = new Conf({ projectName: 'pal-explorer-cli', defaults });
104
+ const configPath = tmpConf.path;
105
+ if (fs.existsSync(configPath)) {
106
+ fs.unlinkSync(configPath);
107
+ }
108
+ } catch {}
109
+ config = new Conf({
110
+ projectName: 'pal-explorer-cli',
111
+ defaults,
112
+ ...(encryptionKey ? { encryptionKey } : {}),
113
+ });
114
+ }
115
+
116
+ // LOW-1 fix: set restrictive permissions on config file (Linux/macOS)
117
+ if (process.platform !== 'win32') {
118
+ try {
119
+ fs.chmodSync(config.path, 0o600);
120
+ } catch {}
121
+ }
122
+
123
+ export default config;
@@ -0,0 +1,87 @@
1
+ import crypto from 'crypto';
2
+ import os from 'os';
3
+ import keytar from 'keytar';
4
+ import config from './config.js';
5
+
6
+ const KEYTAR_SERVICE = 'pal-explorer-config';
7
+ const KEYTAR_ACCOUNT = 'hmac-key';
8
+ const HMAC_STORE_KEY = '_integrity';
9
+
10
+ // Protected config keys that get HMAC integrity checks
11
+ const PROTECTED_KEYS = ['shares', 'users', 'billing', 'billing_license_key'];
12
+
13
+ let hmacKey = null;
14
+
15
+ function deriveMachineKey() {
16
+ const hostname = os.hostname();
17
+ const username = os.userInfo().username;
18
+ const cpus = os.cpus();
19
+ const hardwareId = cpus.length > 0 ? cpus[0].model : 'unknown';
20
+ const seed = `pal-explorer:${hostname}:${username}:${hardwareId}`;
21
+ return crypto.createHash('sha256').update(seed).digest('hex');
22
+ }
23
+
24
+ async function getHmacKey() {
25
+ if (hmacKey) return hmacKey;
26
+ try {
27
+ hmacKey = await keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);
28
+ if (!hmacKey) {
29
+ hmacKey = crypto.randomBytes(32).toString('hex');
30
+ await keytar.setPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT, hmacKey);
31
+ }
32
+ } catch {
33
+ // keytar unavailable (e.g., Linux without libsecret) — derive from machine identity
34
+ hmacKey = deriveMachineKey();
35
+ }
36
+ return hmacKey;
37
+ }
38
+
39
+ function computeHmac(key, data, secret) {
40
+ const hmac = crypto.createHmac('sha256', secret);
41
+ hmac.update(key + ':');
42
+ hmac.update(JSON.stringify(data));
43
+ return hmac.digest('hex');
44
+ }
45
+
46
+ export async function signConfigKey(key) {
47
+ if (!PROTECTED_KEYS.includes(key)) return;
48
+ const secret = await getHmacKey();
49
+ if (!secret) return;
50
+
51
+ const data = config.get(key);
52
+ const mac = computeHmac(key, data, secret);
53
+ const integrity = config.get(HMAC_STORE_KEY) || {};
54
+ integrity[key] = mac;
55
+ config.set(HMAC_STORE_KEY, integrity);
56
+ }
57
+
58
+ export async function verifyConfigKey(key) {
59
+ if (!PROTECTED_KEYS.includes(key)) return true;
60
+ const secret = await getHmacKey();
61
+ if (!secret) return true; // keytar unavailable, can't verify
62
+
63
+ const integrity = config.get(HMAC_STORE_KEY) || {};
64
+ const storedMac = integrity[key];
65
+ if (!storedMac) {
66
+ // First run or migration — sign it now
67
+ await signConfigKey(key);
68
+ return true;
69
+ }
70
+
71
+ const data = config.get(key);
72
+ const expected = computeHmac(key, data, secret);
73
+ return crypto.timingSafeEqual(Buffer.from(storedMac, 'hex'), Buffer.from(expected, 'hex'));
74
+ }
75
+
76
+ export async function setProtected(key, value) {
77
+ config.set(key, value);
78
+ await signConfigKey(key);
79
+ }
80
+
81
+ export async function getProtected(key) {
82
+ const valid = await verifyConfigKey(key);
83
+ if (!valid) {
84
+ throw new Error(`Config integrity check failed for "${key}". Data may have been tampered with.`);
85
+ }
86
+ return config.get(key);
87
+ }
@@ -0,0 +1,9 @@
1
+ import path from 'path';
2
+ import config from './config.js';
3
+
4
+ export function getDownloadDir() {
5
+ const settings = config.get('settings') || {};
6
+ if (settings.downloadDir) return settings.downloadDir;
7
+ const home = process.env.HOME || process.env.USERPROFILE || '';
8
+ return path.join(home, 'Downloads', 'Palexplorer');
9
+ }
@@ -0,0 +1,83 @@
1
+ import regedit from 'regedit';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import fs from 'fs';
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+
8
+ // Note: regedit requires vbs scripts which it bundle, but sometimes we need to set the path
9
+ // regedit.setExternalVBSLocation(path.join(__dirname, 'vbs'));
10
+
11
+ export async function installExplorerContextMenu() {
12
+ if (process.platform !== 'win32') {
13
+ console.log('Explorer integration is only available on Windows.');
14
+ return;
15
+ }
16
+
17
+ const exePath = process.execPath;
18
+ const scriptPath = path.resolve(__dirname, '../../bin/pal.js');
19
+ // Command to run: pe share "filepath"
20
+ // For the GUI, we might want a specific handler that opens the GUI
21
+ const command = `"${exePath}" "${scriptPath}" gui-share "%1"`;
22
+
23
+ const keys = {
24
+ 'HKCU\Software\Classes\*\shell\PalExplorer': {
25
+ 'MUIVerb': {
26
+ value: 'Share with Pal...',
27
+ type: 'REG_SZ'
28
+ },
29
+ 'Icon': {
30
+ value: exePath,
31
+ type: 'REG_SZ'
32
+ }
33
+ },
34
+ 'HKCU\Software\Classes\*\shell\PalExplorer\command': {
35
+ 'default': {
36
+ value: command,
37
+ type: 'REG_SZ'
38
+ }
39
+ },
40
+ 'HKCU\Software\Classes\Directory\shell\PalExplorer': {
41
+ 'MUIVerb': {
42
+ value: 'Share with Pal...',
43
+ type: 'REG_SZ'
44
+ },
45
+ 'Icon': {
46
+ value: exePath,
47
+ type: 'REG_SZ'
48
+ }
49
+ },
50
+ 'HKCU\Software\Classes\Directory\shell\PalExplorer\command': {
51
+ 'default': {
52
+ value: command,
53
+ type: 'REG_SZ'
54
+ }
55
+ }
56
+ };
57
+
58
+ return new Promise((resolve, reject) => {
59
+ regedit.createKey(Object.keys(keys), (err) => {
60
+ if (err) return reject(err);
61
+ regedit.putValue(keys, (err) => {
62
+ if (err) return reject(err);
63
+ resolve();
64
+ });
65
+ });
66
+ });
67
+ }
68
+
69
+ export async function uninstallExplorerContextMenu() {
70
+ if (process.platform !== 'win32') return;
71
+
72
+ const keys = [
73
+ 'HKCU\Software\Classes\*\shell\PalExplorer',
74
+ 'HKCU\Software\Classes\Directory\shell\PalExplorer'
75
+ ];
76
+
77
+ return new Promise((resolve, reject) => {
78
+ regedit.deleteKey(keys, (err) => {
79
+ if (err) return reject(err);
80
+ resolve();
81
+ });
82
+ });
83
+ }
@@ -0,0 +1,12 @@
1
+ export function formatSize(bytes) {
2
+ if (bytes == null) return '';
3
+ if (bytes === 0) return '0 B';
4
+ if (bytes < 1024) return `${bytes} B`;
5
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
6
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
7
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
8
+ }
9
+
10
+ export function formatSpeed(bytesPerSec) {
11
+ return formatSize(bytesPerSec) + '/s';
12
+ }