bytifi 0.1.1 → 0.1.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 +98 -11
- package/bin/bytifi.js +365 -48
- package/lib/crypto.js +43 -0
- package/lib/decrypt.js +664 -0
- package/lib/upload.js +118 -18
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# Bytifi CLI
|
|
2
2
|
|
|
3
|
-
Official command-line tool for encrypting and
|
|
3
|
+
Official command-line tool for encrypting, uploading, and decrypting files with [Bytifi](https://bytifi.com).
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
|
+
### npm (all platforms)
|
|
8
|
+
|
|
7
9
|
```bash
|
|
8
10
|
npm install -g bytifi
|
|
9
11
|
```
|
|
10
12
|
|
|
11
13
|
Requires **Node.js 18+**.
|
|
12
14
|
|
|
13
|
-
Or
|
|
15
|
+
Or from source:
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
18
|
git clone https://github.com/jpwcguy/Bytifi.git
|
|
@@ -18,16 +20,37 @@ cd Bytifi
|
|
|
18
20
|
npm link
|
|
19
21
|
```
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
### Windows (WinGet)
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
winget install Bytifi.Bytifi
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or download [bytifi.exe from GitHub Releases](https://github.com/jpwcguy/Bytifi/releases/latest).
|
|
30
|
+
|
|
31
|
+
### Environment variable (all platforms)
|
|
22
32
|
|
|
23
33
|
```bash
|
|
24
34
|
export BYTIFI_API_KEY=usk_your_api_key_here
|
|
25
35
|
```
|
|
26
36
|
|
|
37
|
+
PowerShell:
|
|
38
|
+
|
|
39
|
+
```powershell
|
|
40
|
+
$env:BYTIFI_API_KEY = "usk_your_api_key_here"
|
|
41
|
+
```
|
|
42
|
+
|
|
27
43
|
Create an API key in **Account → API** on bytifi.com.
|
|
28
44
|
|
|
45
|
+
## Setup
|
|
46
|
+
|
|
47
|
+
Set your API key via environment variable (see Install above) or pass `--api-key` per command.
|
|
48
|
+
Prefer the environment variable — keys on the command line can appear in shell history and process lists.
|
|
49
|
+
|
|
29
50
|
## Usage
|
|
30
51
|
|
|
52
|
+
### Upload
|
|
53
|
+
|
|
31
54
|
```bash
|
|
32
55
|
bytifi upload ./photo.png
|
|
33
56
|
bytifi upload ./photo.png --api-key usk_your_api_key_here
|
|
@@ -35,6 +58,42 @@ bytifi upload ./photo.png --expires 60 --json
|
|
|
35
58
|
bytifi upload ./large.iso -q
|
|
36
59
|
```
|
|
37
60
|
|
|
61
|
+
### Decrypt
|
|
62
|
+
|
|
63
|
+
Download and decrypt a shared file locally, or decrypt a file you already downloaded from `/f/...`. No API key required.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# From share URL (downloads + decrypts)
|
|
67
|
+
bytifi decrypt 'https://bytifi.com/link?link=TOKEN#token=ENCRYPTION_TOKEN'
|
|
68
|
+
bytifi decrypt TOKEN --token ENCRYPTION_TOKEN -o ./restored-file.pdf
|
|
69
|
+
|
|
70
|
+
# From a downloaded encrypted file
|
|
71
|
+
bytifi decrypt ./downloaded.encrypted --link TOKEN --token ENCRYPTION_TOKEN
|
|
72
|
+
bytifi decrypt ./downloaded.encrypted --meta ./upload-meta.json --token ENCRYPTION_TOKEN
|
|
73
|
+
bytifi decrypt ./parts/ --link TOKEN --token ENCRYPTION_TOKEN
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Use the full share URL (with `#token=...`) when possible. For `/f/...` downloads, pass the encryption token with `--token` and either `--link` (fetches metadata from the API) or `--meta` (saved `clientEncryptionMeta` JSON from upload `--json` output).
|
|
77
|
+
|
|
78
|
+
#### Offline decrypt workflow
|
|
79
|
+
|
|
80
|
+
Save metadata when you upload, so you can decrypt a downloaded `/f/...` file even after the link expires:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# 1. Upload and save the JSON output
|
|
84
|
+
bytifi upload ./report.pdf --json > upload.json
|
|
85
|
+
|
|
86
|
+
# 2. Download the encrypted file manually (browser, curl, etc.)
|
|
87
|
+
curl -L "$(jq -r .encryptedFile upload.json)" -o report.encrypted
|
|
88
|
+
|
|
89
|
+
# 3. Save metadata and decrypt locally (no API call needed)
|
|
90
|
+
jq -c .clientEncryptionMeta upload.json > report.meta.json
|
|
91
|
+
bytifi decrypt ./report.encrypted \
|
|
92
|
+
--meta ./report.meta.json \
|
|
93
|
+
--token "$(jq -r .encryptionToken upload.json)" \
|
|
94
|
+
-o ./report.pdf
|
|
95
|
+
```
|
|
96
|
+
|
|
38
97
|
Without a global install:
|
|
39
98
|
|
|
40
99
|
```bash
|
|
@@ -44,7 +103,7 @@ npm exec bytifi -- upload ./photo.png --api-key usk_your_api_key_here
|
|
|
44
103
|
|
|
45
104
|
Note: with `npm exec`, put `--` before the file path so npm does not swallow `--api-key`.
|
|
46
105
|
|
|
47
|
-
###
|
|
106
|
+
### Upload options
|
|
48
107
|
|
|
49
108
|
| Flag | Description |
|
|
50
109
|
|------|-------------|
|
|
@@ -53,22 +112,50 @@ Note: with `npm exec`, put `--` before the file path so npm does not swallow `--
|
|
|
53
112
|
| `--delete-on-download` | Delete after first download |
|
|
54
113
|
| `--json` | Machine-readable JSON output |
|
|
55
114
|
| `-q, --quiet` | Print only the share URL |
|
|
56
|
-
| `--
|
|
115
|
+
| `--verbose` | Print API error details to stderr |
|
|
116
|
+
| `--mime-type` | Override detected MIME type |
|
|
117
|
+
| `--base-url` | API base URL (default: `https://bytifi.com`) |
|
|
118
|
+
|
|
119
|
+
### Decrypt options
|
|
120
|
+
|
|
121
|
+
| Flag | Description |
|
|
122
|
+
|------|-------------|
|
|
123
|
+
| `--token` | Encryption token (required if not in URL `#token=...`) |
|
|
124
|
+
| `--link` | Link token for metadata when decrypting a local encrypted file |
|
|
125
|
+
| `--meta` | Saved `clientEncryptionMeta` JSON for offline local decrypt |
|
|
126
|
+
| `--share-url` | Share URL to read token/metadata while decrypting a local file |
|
|
127
|
+
| `-o, --output` | Output file path |
|
|
128
|
+
| `--output-dir` | Output directory when saving under the original filename |
|
|
129
|
+
| `--json` | Machine-readable JSON output |
|
|
130
|
+
| `-q, --quiet` | Print only the output file path |
|
|
131
|
+
| `--verbose` | Print error details to stderr |
|
|
132
|
+
| `--base-url` | API base URL (default: `https://bytifi.com`) |
|
|
133
|
+
|
|
134
|
+
Exit codes: `0` success, `1` usage error, `2` API error, `3` network error.
|
|
135
|
+
|
|
136
|
+
JSON output (`--json`) for upload includes `shareUrl`, `encryptedFile`, `encryptionToken`, `clientEncryptionMeta`, `expiresAt`, and `token`.
|
|
137
|
+
|
|
138
|
+
JSON output for decrypt includes `outputPath`, `originalName`, `size`, `mimeType`, `expiresAt`, `linkToken`, `storageMode`, and `sourcePath` (for local decrypt).
|
|
139
|
+
|
|
140
|
+
Files over ~100 MB encrypted use multipart upload automatically. Progress prints to stderr unless `--json` or `--quiet` is set.
|
|
57
141
|
|
|
58
142
|
## How it works
|
|
59
143
|
|
|
144
|
+
**Upload**
|
|
145
|
+
|
|
60
146
|
1. Encrypts the file locally with AES-GCM (same format as the website)
|
|
61
|
-
2. Uploads encrypted bytes via the public API
|
|
147
|
+
2. Uploads encrypted bytes via the public API (parallel part uploads for large files)
|
|
62
148
|
3. Prints a share URL including `#token=...`
|
|
63
149
|
|
|
64
|
-
|
|
150
|
+
**Decrypt**
|
|
151
|
+
|
|
152
|
+
1. Reads link metadata from `/api/link/:token`, or from a saved `--meta` JSON file
|
|
153
|
+
2. Downloads the encrypted file (single blob or per-part for large uploads), unless you pass a local encrypted file
|
|
154
|
+
3. Decrypts locally and writes the original file to disk
|
|
65
155
|
|
|
66
156
|
## Development
|
|
67
157
|
|
|
68
158
|
```bash
|
|
69
159
|
node bin/bytifi.js upload ./file.png --json
|
|
160
|
+
node bin/bytifi.js decrypt ./file.encrypted --meta ./meta.json --token '...'
|
|
70
161
|
```
|
|
71
|
-
|
|
72
|
-
## Status
|
|
73
|
-
|
|
74
|
-
WIP — v0.1.0 direct + multipart upload supported.
|
package/bin/bytifi.js
CHANGED
|
@@ -3,34 +3,73 @@
|
|
|
3
3
|
import fs from 'node:fs/promises'
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import process from 'node:process'
|
|
6
|
-
import {
|
|
6
|
+
import { createRequire } from 'node:module'
|
|
7
|
+
import { decryptFile } from '../lib/decrypt.js'
|
|
7
8
|
import { BytifiApiError, BytifiNetworkError, uploadFile } from '../lib/upload.js'
|
|
8
9
|
|
|
9
|
-
const
|
|
10
|
+
const require = createRequire(import.meta.url)
|
|
11
|
+
const { version } = require('../package.json')
|
|
10
12
|
|
|
11
13
|
function printHelp() {
|
|
12
|
-
process.stdout.write(`Bytifi CLI — encrypt and
|
|
14
|
+
process.stdout.write(`Bytifi CLI v${version} — encrypt, upload, and decrypt files
|
|
13
15
|
|
|
14
16
|
Usage:
|
|
15
17
|
bytifi upload <file> [options]
|
|
18
|
+
bytifi decrypt <url-or-token> [options]
|
|
19
|
+
bytifi decrypt <encrypted-file> [options]
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
-k, --api-key <key>
|
|
19
|
-
-e, --expires <minutes>
|
|
21
|
+
Upload options:
|
|
22
|
+
-k, --api-key <key> API key (default: BYTIFI_API_KEY env var)
|
|
23
|
+
-e, --expires <minutes> Link lifetime: 5|15|30|60|120 (default: 30)
|
|
20
24
|
--delete-on-download Remove file after first download
|
|
21
25
|
--json Print machine-readable JSON to stdout
|
|
22
26
|
-q, --quiet Print only the share URL
|
|
27
|
+
--verbose Show API error details on stderr
|
|
23
28
|
--mime-type <type> Override detected MIME type
|
|
24
29
|
--base-url <url> API base URL (default: https://bytifi.com)
|
|
30
|
+
|
|
31
|
+
Decrypt options:
|
|
32
|
+
--token <token> Encryption token (if not in URL #token=...)
|
|
33
|
+
--link <token> Link token for metadata (local encrypted files)
|
|
34
|
+
--meta <path> Saved clientEncryptionMeta JSON (offline local decrypt)
|
|
35
|
+
--share-url <url> Share URL for token/metadata when decrypting a local file
|
|
36
|
+
-o, --output <path> Output file path (default: original filename)
|
|
37
|
+
--output-dir <dir> Directory for decrypted file (default: cwd)
|
|
38
|
+
--json Print machine-readable JSON to stdout
|
|
39
|
+
-q, --quiet Print only the output file path
|
|
40
|
+
--verbose Show error details on stderr
|
|
41
|
+
--base-url <url> API base URL (default: https://bytifi.com)
|
|
42
|
+
|
|
43
|
+
Global:
|
|
44
|
+
-V, --version Show version
|
|
25
45
|
-h, --help Show this help
|
|
26
46
|
|
|
47
|
+
Exit codes:
|
|
48
|
+
0 success
|
|
49
|
+
1 usage or validation error
|
|
50
|
+
2 API error (4xx/5xx response)
|
|
51
|
+
3 network error
|
|
52
|
+
|
|
27
53
|
Examples:
|
|
28
54
|
bytifi upload ./photo.png
|
|
29
55
|
BYTIFI_API_KEY=usk_... bytifi upload report.pdf --expires 60 --json
|
|
56
|
+
bytifi decrypt 'https://bytifi.com/link?link=abc123#token=...'
|
|
57
|
+
bytifi decrypt abc123 --token '...' -o ./report.pdf
|
|
58
|
+
bytifi decrypt ./downloaded.encrypted --link abc123 --token '...'
|
|
59
|
+
bytifi decrypt ./downloaded.encrypted --meta ./upload-meta.json --token '...'
|
|
60
|
+
bytifi decrypt ./parts/ --link abc123 --token '...'
|
|
30
61
|
`)
|
|
31
62
|
}
|
|
32
63
|
|
|
33
|
-
function
|
|
64
|
+
function readFlagValue(argv, index, flagName) {
|
|
65
|
+
const value = argv[index + 1]
|
|
66
|
+
if (!value || value.startsWith('-')) {
|
|
67
|
+
throw new Error(`Option ${flagName} requires a value.`)
|
|
68
|
+
}
|
|
69
|
+
return value
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseUploadArgs(argv) {
|
|
34
73
|
const positional = []
|
|
35
74
|
const options = {
|
|
36
75
|
apiKey: process.env.BYTIFI_API_KEY || '',
|
|
@@ -38,9 +77,11 @@ function parseArgs(argv) {
|
|
|
38
77
|
deleteOnDownload: false,
|
|
39
78
|
json: false,
|
|
40
79
|
quiet: false,
|
|
80
|
+
verbose: false,
|
|
41
81
|
mimeType: '',
|
|
42
82
|
baseUrl: 'https://bytifi.com',
|
|
43
83
|
help: false,
|
|
84
|
+
version: false,
|
|
44
85
|
}
|
|
45
86
|
|
|
46
87
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -51,6 +92,11 @@ function parseArgs(argv) {
|
|
|
51
92
|
continue
|
|
52
93
|
}
|
|
53
94
|
|
|
95
|
+
if (arg === '--version' || arg === '-V') {
|
|
96
|
+
options.version = true
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
|
|
54
100
|
if (arg === '--json') {
|
|
55
101
|
options.json = true
|
|
56
102
|
continue
|
|
@@ -61,31 +107,138 @@ function parseArgs(argv) {
|
|
|
61
107
|
continue
|
|
62
108
|
}
|
|
63
109
|
|
|
110
|
+
if (arg === '--verbose') {
|
|
111
|
+
options.verbose = true
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
|
|
64
115
|
if (arg === '--delete-on-download') {
|
|
65
116
|
options.deleteOnDownload = true
|
|
66
117
|
continue
|
|
67
118
|
}
|
|
68
119
|
|
|
69
120
|
if (arg === '--api-key' || arg === '-k') {
|
|
70
|
-
options.apiKey = argv
|
|
121
|
+
options.apiKey = readFlagValue(argv, index, arg)
|
|
71
122
|
index += 1
|
|
72
123
|
continue
|
|
73
124
|
}
|
|
74
125
|
|
|
75
126
|
if (arg === '--expires' || arg === '-e') {
|
|
76
|
-
|
|
127
|
+
const raw = readFlagValue(argv, index, arg)
|
|
128
|
+
const minutes = Number(raw)
|
|
129
|
+
if (!Number.isFinite(minutes)) {
|
|
130
|
+
throw new Error(`Invalid expires value: ${raw}`)
|
|
131
|
+
}
|
|
132
|
+
options.expiresInMinutes = minutes
|
|
77
133
|
index += 1
|
|
78
134
|
continue
|
|
79
135
|
}
|
|
80
136
|
|
|
81
137
|
if (arg === '--mime-type') {
|
|
82
|
-
options.mimeType = argv
|
|
138
|
+
options.mimeType = readFlagValue(argv, index, arg)
|
|
83
139
|
index += 1
|
|
84
140
|
continue
|
|
85
141
|
}
|
|
86
142
|
|
|
87
143
|
if (arg === '--base-url') {
|
|
88
|
-
options.baseUrl = argv
|
|
144
|
+
options.baseUrl = readFlagValue(argv, index, arg)
|
|
145
|
+
index += 1
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (arg.startsWith('-')) {
|
|
150
|
+
throw new Error(`Unknown option: ${arg}`)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
positional.push(arg)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { positional, options }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseDecryptArgs(argv) {
|
|
160
|
+
const positional = []
|
|
161
|
+
const options = {
|
|
162
|
+
encryptionToken: '',
|
|
163
|
+
linkToken: '',
|
|
164
|
+
metaPath: '',
|
|
165
|
+
shareUrl: '',
|
|
166
|
+
output: '',
|
|
167
|
+
outputDirectory: '',
|
|
168
|
+
json: false,
|
|
169
|
+
quiet: false,
|
|
170
|
+
verbose: false,
|
|
171
|
+
baseUrl: 'https://bytifi.com',
|
|
172
|
+
help: false,
|
|
173
|
+
version: false,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
177
|
+
const arg = argv[index]
|
|
178
|
+
|
|
179
|
+
if (arg === '--help' || arg === '-h') {
|
|
180
|
+
options.help = true
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (arg === '--version' || arg === '-V') {
|
|
185
|
+
options.version = true
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (arg === '--json') {
|
|
190
|
+
options.json = true
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (arg === '--quiet' || arg === '-q') {
|
|
195
|
+
options.quiet = true
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (arg === '--verbose') {
|
|
200
|
+
options.verbose = true
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (arg === '--token') {
|
|
205
|
+
options.encryptionToken = readFlagValue(argv, index, arg)
|
|
206
|
+
index += 1
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (arg === '--link') {
|
|
211
|
+
options.linkToken = readFlagValue(argv, index, arg)
|
|
212
|
+
index += 1
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (arg === '--meta') {
|
|
217
|
+
options.metaPath = readFlagValue(argv, index, arg)
|
|
218
|
+
index += 1
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (arg === '--share-url') {
|
|
223
|
+
options.shareUrl = readFlagValue(argv, index, arg)
|
|
224
|
+
index += 1
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (arg === '--output' || arg === '-o') {
|
|
229
|
+
options.output = readFlagValue(argv, index, arg)
|
|
230
|
+
index += 1
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (arg === '--output-dir') {
|
|
235
|
+
options.outputDirectory = readFlagValue(argv, index, arg)
|
|
236
|
+
index += 1
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (arg === '--base-url') {
|
|
241
|
+
options.baseUrl = readFlagValue(argv, index, arg)
|
|
89
242
|
index += 1
|
|
90
243
|
continue
|
|
91
244
|
}
|
|
@@ -107,6 +260,14 @@ function validateExpires(minutes) {
|
|
|
107
260
|
}
|
|
108
261
|
}
|
|
109
262
|
|
|
263
|
+
function writeProgress(label, percent) {
|
|
264
|
+
process.stderr.write(`\r${label}: ${percent}%`)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function clearProgressLine() {
|
|
268
|
+
process.stderr.write('\r\x1b[K')
|
|
269
|
+
}
|
|
270
|
+
|
|
110
271
|
async function runUpload(filePath, options) {
|
|
111
272
|
validateExpires(options.expiresInMinutes)
|
|
112
273
|
|
|
@@ -115,34 +276,120 @@ async function runUpload(filePath, options) {
|
|
|
115
276
|
}
|
|
116
277
|
|
|
117
278
|
const resolvedPath = path.resolve(filePath)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
expiresInMinutes: options.expiresInMinutes,
|
|
124
|
-
deleteOnDownload: options.deleteOnDownload,
|
|
125
|
-
mimeType: options.mimeType || undefined,
|
|
126
|
-
onProgress: options.quiet || options.json
|
|
127
|
-
? undefined
|
|
128
|
-
: (percent) => {
|
|
129
|
-
process.stderr.write(`Encrypting and uploading: ${percent}%\n`)
|
|
130
|
-
},
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
if (options.json) {
|
|
134
|
-
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
|
|
135
|
-
return
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
await fs.access(resolvedPath)
|
|
282
|
+
} catch {
|
|
283
|
+
throw new Error(`File not found or not readable: ${resolvedPath}`)
|
|
136
284
|
}
|
|
137
285
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
286
|
+
const abortController = new AbortController()
|
|
287
|
+
|
|
288
|
+
const handleSignal = () => {
|
|
289
|
+
abortController.abort()
|
|
141
290
|
}
|
|
142
291
|
|
|
143
|
-
process.
|
|
144
|
-
process.
|
|
145
|
-
|
|
292
|
+
process.on('SIGINT', handleSignal)
|
|
293
|
+
process.on('SIGTERM', handleSignal)
|
|
294
|
+
|
|
295
|
+
const showProgress = !options.quiet && !options.json
|
|
296
|
+
let lastPercent = -1
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const result = await uploadFile(resolvedPath, {
|
|
300
|
+
apiKey: options.apiKey,
|
|
301
|
+
baseUrl: options.baseUrl,
|
|
302
|
+
expiresInMinutes: options.expiresInMinutes,
|
|
303
|
+
deleteOnDownload: options.deleteOnDownload,
|
|
304
|
+
mimeType: options.mimeType || undefined,
|
|
305
|
+
signal: abortController.signal,
|
|
306
|
+
onProgress: showProgress
|
|
307
|
+
? (percent) => {
|
|
308
|
+
if (percent !== lastPercent) {
|
|
309
|
+
lastPercent = percent
|
|
310
|
+
writeProgress('Encrypting and uploading', percent)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
: undefined,
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
if (showProgress) {
|
|
317
|
+
clearProgressLine()
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (options.json) {
|
|
321
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (options.quiet) {
|
|
326
|
+
process.stdout.write(`${result.shareUrl}\n`)
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
process.stdout.write(`Share URL:\n${result.shareUrl}\n`)
|
|
331
|
+
process.stdout.write(`Encrypted file:\n${result.encryptedFile}\n`)
|
|
332
|
+
process.stdout.write(`Expires: ${result.expiresAt}\n`)
|
|
333
|
+
} finally {
|
|
334
|
+
process.off('SIGINT', handleSignal)
|
|
335
|
+
process.off('SIGTERM', handleSignal)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function runDecrypt(input, options) {
|
|
340
|
+
const abortController = new AbortController()
|
|
341
|
+
|
|
342
|
+
const handleSignal = () => {
|
|
343
|
+
abortController.abort()
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
process.on('SIGINT', handleSignal)
|
|
347
|
+
process.on('SIGTERM', handleSignal)
|
|
348
|
+
|
|
349
|
+
const showProgress = !options.quiet && !options.json
|
|
350
|
+
let lastPercent = -1
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
const result = await decryptFile(input, {
|
|
354
|
+
encryptionToken: options.encryptionToken,
|
|
355
|
+
linkToken: options.linkToken,
|
|
356
|
+
metaPath: options.metaPath,
|
|
357
|
+
shareUrl: options.shareUrl,
|
|
358
|
+
output: options.output || undefined,
|
|
359
|
+
outputDirectory: options.outputDirectory || undefined,
|
|
360
|
+
baseUrl: options.baseUrl,
|
|
361
|
+
signal: abortController.signal,
|
|
362
|
+
onProgress: showProgress
|
|
363
|
+
? (percent) => {
|
|
364
|
+
if (percent !== lastPercent) {
|
|
365
|
+
lastPercent = percent
|
|
366
|
+
writeProgress('Downloading and decrypting', percent)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
: undefined,
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
if (showProgress) {
|
|
373
|
+
clearProgressLine()
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (options.json) {
|
|
377
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
|
|
378
|
+
return
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (options.quiet) {
|
|
382
|
+
process.stdout.write(`${result.outputPath}\n`)
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
process.stdout.write(`Saved: ${result.outputPath}\n`)
|
|
387
|
+
process.stdout.write(`Original name: ${result.originalName}\n`)
|
|
388
|
+
process.stdout.write(`Expires: ${result.expiresAt}\n`)
|
|
389
|
+
} finally {
|
|
390
|
+
process.off('SIGINT', handleSignal)
|
|
391
|
+
process.off('SIGTERM', handleSignal)
|
|
392
|
+
}
|
|
146
393
|
}
|
|
147
394
|
|
|
148
395
|
function exitCodeForError(error) {
|
|
@@ -151,6 +398,25 @@ function exitCodeForError(error) {
|
|
|
151
398
|
return 1
|
|
152
399
|
}
|
|
153
400
|
|
|
401
|
+
function printError(error, verbose) {
|
|
402
|
+
process.stderr.write(`${error.message || 'Command failed.'}\n`)
|
|
403
|
+
|
|
404
|
+
if (!verbose) return
|
|
405
|
+
|
|
406
|
+
if (error instanceof BytifiApiError) {
|
|
407
|
+
if (error.status) {
|
|
408
|
+
process.stderr.write(`HTTP ${error.status}\n`)
|
|
409
|
+
}
|
|
410
|
+
if (error.body) {
|
|
411
|
+
process.stderr.write(`${JSON.stringify(error.body, null, 2)}\n`)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (error instanceof BytifiNetworkError && error.cause) {
|
|
416
|
+
process.stderr.write(`${error.cause}\n`)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
154
420
|
async function main() {
|
|
155
421
|
const [command, ...rest] = process.argv.slice(2)
|
|
156
422
|
|
|
@@ -159,31 +425,82 @@ async function main() {
|
|
|
159
425
|
process.exit(0)
|
|
160
426
|
}
|
|
161
427
|
|
|
162
|
-
if (command
|
|
163
|
-
|
|
428
|
+
if (command === '--version' || command === '-V') {
|
|
429
|
+
process.stdout.write(`${version}\n`)
|
|
430
|
+
process.exit(0)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (command === 'help') {
|
|
434
|
+
printHelp()
|
|
435
|
+
process.exit(0)
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (command === 'upload') {
|
|
439
|
+
const { positional, options } = parseUploadArgs(rest)
|
|
440
|
+
|
|
441
|
+
if (options.help) {
|
|
164
442
|
printHelp()
|
|
165
443
|
process.exit(0)
|
|
166
444
|
}
|
|
167
445
|
|
|
168
|
-
|
|
169
|
-
|
|
446
|
+
if (options.version) {
|
|
447
|
+
process.stdout.write(`${version}\n`)
|
|
448
|
+
process.exit(0)
|
|
449
|
+
}
|
|
170
450
|
|
|
171
|
-
|
|
451
|
+
if (options.json && options.quiet) {
|
|
452
|
+
throw new Error('Use either --json or --quiet, not both.')
|
|
453
|
+
}
|
|
172
454
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
455
|
+
const filePath = positional[0]
|
|
456
|
+
if (!filePath) {
|
|
457
|
+
throw new Error('Missing file path. Usage: bytifi upload <file>')
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (positional.length > 1) {
|
|
461
|
+
process.stderr.write(`Warning: ignoring extra files: ${positional.slice(1).join(', ')}\n`)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
await runUpload(filePath, options)
|
|
465
|
+
return
|
|
176
466
|
}
|
|
177
467
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
468
|
+
if (command === 'decrypt') {
|
|
469
|
+
const { positional, options } = parseDecryptArgs(rest)
|
|
470
|
+
|
|
471
|
+
if (options.help) {
|
|
472
|
+
printHelp()
|
|
473
|
+
process.exit(0)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (options.version) {
|
|
477
|
+
process.stdout.write(`${version}\n`)
|
|
478
|
+
process.exit(0)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (options.json && options.quiet) {
|
|
482
|
+
throw new Error('Use either --json or --quiet, not both.')
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const input = positional[0]
|
|
486
|
+
if (!input) {
|
|
487
|
+
throw new Error('Missing input. Usage: bytifi decrypt <url-or-token|encrypted-file>')
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (positional.length > 1) {
|
|
491
|
+
process.stderr.write(`Warning: ignoring extra arguments: ${positional.slice(1).join(', ')}\n`)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
await runDecrypt(input, options)
|
|
495
|
+
return
|
|
181
496
|
}
|
|
182
497
|
|
|
183
|
-
|
|
498
|
+
throw new Error(`Unknown command: ${command}`)
|
|
184
499
|
}
|
|
185
500
|
|
|
186
501
|
main().catch((error) => {
|
|
187
|
-
|
|
502
|
+
clearProgressLine()
|
|
503
|
+
const verbose = process.argv.includes('--verbose')
|
|
504
|
+
printError(error, verbose)
|
|
188
505
|
process.exit(exitCodeForError(error))
|
|
189
506
|
})
|