bytifi 0.2.0 → 0.2.1
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 +5 -7
- package/bin/bytifi.js +7 -16
- package/lib/crypto.js +6 -68
- package/lib/upload.js +0 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,11 +55,11 @@ Prefer the environment variable — keys on the command line can appear in shell
|
|
|
55
55
|
bytifi upload ./photo.png
|
|
56
56
|
bytifi upload "./my video (1).mp4"
|
|
57
57
|
bytifi upload ./report.pdf --expires 60 --delete-on-download
|
|
58
|
-
bytifi upload ./logs.txt --
|
|
58
|
+
bytifi upload ./logs.txt --concurrency 8 --json > upload.json
|
|
59
59
|
bytifi upload ./photo.png -q
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
Files over **10 MB** use multipart upload automatically
|
|
62
|
+
All uploads are gzip-compressed per chunk before encryption. Files over **10 MB** use multipart upload automatically.
|
|
63
63
|
|
|
64
64
|
Upload accepts **one file at a time**. Quote paths that contain spaces. Avoid shell globs like `**` — your shell may expand them into dozens of paths.
|
|
65
65
|
|
|
@@ -128,7 +128,6 @@ Note: with `npm exec`, put `--` before the file path so npm does not swallow `--
|
|
|
128
128
|
| `-q, --quiet` | Print only the share URL |
|
|
129
129
|
| `--verbose` | Print API error details to stderr |
|
|
130
130
|
| `--mime-type` | Override detected MIME type |
|
|
131
|
-
| `--compress` | Compression mode: `auto`, `gzip`, or `off` (default: `auto`) |
|
|
132
131
|
| `--concurrency` | Parallel encrypt/upload workers, 1–16 (default: `4`) |
|
|
133
132
|
| `--base-url` | API base URL (default: `https://bytifi.com`) |
|
|
134
133
|
|
|
@@ -160,10 +159,9 @@ Files over ~100 MB encrypted use multipart upload automatically. Progress prints
|
|
|
160
159
|
|
|
161
160
|
**Upload**
|
|
162
161
|
|
|
163
|
-
1.
|
|
164
|
-
2.
|
|
165
|
-
3.
|
|
166
|
-
4. Prints a share URL including `#token=...`
|
|
162
|
+
1. Gzip-compresses each chunk, then encrypts locally with AES-GCM (meta `version: 2`)
|
|
163
|
+
2. Uploads encrypted bytes via multipart pipeline for files >10 MB
|
|
164
|
+
3. Prints a share URL including `#token=...`
|
|
167
165
|
|
|
168
166
|
## Compatibility
|
|
169
167
|
|
package/bin/bytifi.js
CHANGED
|
@@ -26,7 +26,6 @@ Upload options:
|
|
|
26
26
|
-q, --quiet Print only the share URL
|
|
27
27
|
--verbose Show API error details on stderr
|
|
28
28
|
--mime-type <type> Override detected MIME type
|
|
29
|
-
--compress <mode> Compression: auto|gzip|off (default: auto)
|
|
30
29
|
--concurrency <n> Parallel part workers (default: 4)
|
|
31
30
|
--base-url <url> API base URL (default: https://bytifi.com)
|
|
32
31
|
|
|
@@ -54,16 +53,16 @@ Exit codes:
|
|
|
54
53
|
3 network error
|
|
55
54
|
|
|
56
55
|
Examples:
|
|
56
|
+
bytifi --version
|
|
57
|
+
export BYTIFI_API_KEY=usk_your_key
|
|
58
|
+
|
|
57
59
|
bytifi upload ./photo.png
|
|
58
|
-
bytifi upload ./
|
|
59
|
-
bytifi upload ./logs.
|
|
60
|
+
bytifi upload "./my report.pdf" --expires 60 --delete-on-download
|
|
61
|
+
bytifi upload ./logs.txt --concurrency 8 --json > upload.json
|
|
60
62
|
bytifi upload ./large.iso -q
|
|
61
63
|
|
|
62
|
-
bytifi decrypt 'https://bytifi.com/link?link=
|
|
63
|
-
bytifi decrypt
|
|
64
|
-
|
|
65
|
-
bytifi decrypt ./downloaded.encrypted --upload-json upload.json
|
|
66
|
-
bytifi decrypt "./my file(1).mp4" --link abc --token 'ENCRYPTION_TOKEN'
|
|
64
|
+
bytifi decrypt 'https://bytifi.com/link?link=LINK#token=KEY'
|
|
65
|
+
bytifi decrypt ./downloaded.bin --upload-json upload.json -o ./restored.bin
|
|
67
66
|
`)
|
|
68
67
|
}
|
|
69
68
|
|
|
@@ -85,7 +84,6 @@ function parseUploadArgs(argv) {
|
|
|
85
84
|
quiet: false,
|
|
86
85
|
verbose: false,
|
|
87
86
|
mimeType: '',
|
|
88
|
-
compressionMode: 'auto',
|
|
89
87
|
concurrency: 4,
|
|
90
88
|
baseUrl: 'https://bytifi.com',
|
|
91
89
|
help: false,
|
|
@@ -148,12 +146,6 @@ function parseUploadArgs(argv) {
|
|
|
148
146
|
continue
|
|
149
147
|
}
|
|
150
148
|
|
|
151
|
-
if (arg === '--compress') {
|
|
152
|
-
options.compressionMode = readFlagValue(argv, index, arg)
|
|
153
|
-
index += 1
|
|
154
|
-
continue
|
|
155
|
-
}
|
|
156
|
-
|
|
157
149
|
if (arg === '--concurrency') {
|
|
158
150
|
const raw = readFlagValue(argv, index, arg)
|
|
159
151
|
const concurrency = Number(raw)
|
|
@@ -334,7 +326,6 @@ async function runUpload(filePath, options) {
|
|
|
334
326
|
expiresInMinutes: options.expiresInMinutes,
|
|
335
327
|
deleteOnDownload: options.deleteOnDownload,
|
|
336
328
|
mimeType: options.mimeType || undefined,
|
|
337
|
-
compressionMode: options.compressionMode,
|
|
338
329
|
concurrency: options.concurrency,
|
|
339
330
|
signal: abortController.signal,
|
|
340
331
|
onProgress: showProgress
|
package/lib/crypto.js
CHANGED
|
@@ -13,19 +13,6 @@ export const PLAIN_CHUNK_SIZE = ENCRYPTED_PART_SIZE - 16
|
|
|
13
13
|
export const DIRECT_UPLOAD_LIMIT_BYTES = 100 * 1024 * 1024
|
|
14
14
|
export const MULTIPART_THRESHOLD_BYTES = 10 * 1024 * 1024
|
|
15
15
|
|
|
16
|
-
const SKIP_COMPRESS_EXTENSIONS = new Set([
|
|
17
|
-
'.7z', '.avi', '.br', '.bz2', '.gif', '.gz', '.jpeg', '.jpg', '.mkv', '.mov',
|
|
18
|
-
'.mp3', '.mp4', '.png', '.rar', '.webp', '.woff', '.woff2', '.xz', '.zip', '.zst', '.iso',
|
|
19
|
-
])
|
|
20
|
-
|
|
21
|
-
const SKIP_COMPRESS_MIME_EXACT = new Set([
|
|
22
|
-
'application/gzip',
|
|
23
|
-
'application/pdf',
|
|
24
|
-
'application/x-7z-compressed',
|
|
25
|
-
'application/x-rar-compressed',
|
|
26
|
-
'application/zip',
|
|
27
|
-
])
|
|
28
|
-
|
|
29
16
|
export function buildChunkIv(noncePrefix, chunkIndex) {
|
|
30
17
|
const iv = Buffer.alloc(12)
|
|
31
18
|
noncePrefix.copy(iv, 0, 0, 8)
|
|
@@ -33,44 +20,8 @@ export function buildChunkIv(noncePrefix, chunkIndex) {
|
|
|
33
20
|
return iv
|
|
34
21
|
}
|
|
35
22
|
|
|
36
|
-
export function resolveCompressionMode(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (mode === 'off' || mode === 'none') {
|
|
40
|
-
return 'none'
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (mode === 'gzip') {
|
|
44
|
-
return 'gzip'
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (mode === 'auto') {
|
|
48
|
-
return shouldSkipAutoCompression(mimeType, originalName) ? 'none' : 'gzip'
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
throw new Error('compress must be one of: auto, gzip, off')
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function shouldSkipAutoCompression(mimeType, originalName) {
|
|
55
|
-
const extension = path.extname(String(originalName || '')).toLowerCase()
|
|
56
|
-
if (SKIP_COMPRESS_EXTENSIONS.has(extension)) {
|
|
57
|
-
return true
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const normalizedMime = String(mimeType || '').toLowerCase()
|
|
61
|
-
if (SKIP_COMPRESS_MIME_EXACT.has(normalizedMime)) {
|
|
62
|
-
return true
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (normalizedMime.startsWith('video/') || normalizedMime.startsWith('audio/')) {
|
|
66
|
-
return true
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (normalizedMime.startsWith('image/')) {
|
|
70
|
-
return true
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return false
|
|
23
|
+
export function resolveCompressionMode() {
|
|
24
|
+
return 'gzip'
|
|
74
25
|
}
|
|
75
26
|
|
|
76
27
|
export function usesChunkCompression(meta) {
|
|
@@ -112,17 +63,11 @@ export function buildClientEncryptionMeta({
|
|
|
112
63
|
noncePrefix,
|
|
113
64
|
originalSize,
|
|
114
65
|
mimeType,
|
|
115
|
-
compression = 'none',
|
|
116
66
|
}) {
|
|
117
|
-
const compressionMeta = normalizeCompressionMeta({
|
|
118
|
-
algorithm: compression === 'gzip' ? 'gzip' : 'none',
|
|
119
|
-
scope: 'chunk',
|
|
120
|
-
})
|
|
121
|
-
|
|
122
67
|
return {
|
|
123
|
-
version:
|
|
68
|
+
version: 2,
|
|
124
69
|
algorithm: 'AES-GCM',
|
|
125
|
-
compression:
|
|
70
|
+
compression: { algorithm: 'gzip', scope: 'chunk' },
|
|
126
71
|
chunkSize: plainChunkSize,
|
|
127
72
|
chunkCount,
|
|
128
73
|
noncePrefix: toBase64Url(noncePrefix),
|
|
@@ -151,12 +96,10 @@ export function createEncryptionContext({
|
|
|
151
96
|
tokenBytes = crypto.randomBytes(32),
|
|
152
97
|
noncePrefix = crypto.randomBytes(8),
|
|
153
98
|
plainChunkSize = PLAIN_CHUNK_SIZE,
|
|
154
|
-
compressionMode = 'auto',
|
|
155
99
|
}) {
|
|
156
100
|
if (tokenBytes.length !== 32) throw new Error('Encryption token must be 32 bytes.')
|
|
157
101
|
if (noncePrefix.length !== 8) throw new Error('Nonce prefix must be 8 bytes.')
|
|
158
102
|
|
|
159
|
-
const compression = resolveCompressionMode(compressionMode, mimeType, originalName)
|
|
160
103
|
const { chunkCount, encryptedSize } = calculateEncryptedSize(originalSize, plainChunkSize)
|
|
161
104
|
const meta = buildClientEncryptionMeta({
|
|
162
105
|
plainChunkSize,
|
|
@@ -164,7 +107,6 @@ export function createEncryptionContext({
|
|
|
164
107
|
noncePrefix,
|
|
165
108
|
originalSize,
|
|
166
109
|
mimeType,
|
|
167
|
-
compression,
|
|
168
110
|
})
|
|
169
111
|
|
|
170
112
|
return {
|
|
@@ -178,7 +120,7 @@ export function createEncryptionContext({
|
|
|
178
120
|
mimeType,
|
|
179
121
|
originalSize,
|
|
180
122
|
plainChunkSize,
|
|
181
|
-
compression,
|
|
123
|
+
compression: 'gzip',
|
|
182
124
|
}
|
|
183
125
|
}
|
|
184
126
|
|
|
@@ -199,7 +141,6 @@ export async function resolveUploadFile(filePath, options = {}) {
|
|
|
199
141
|
originalSize: stat.size,
|
|
200
142
|
originalName,
|
|
201
143
|
mimeType,
|
|
202
|
-
compressionMode: options.compressionMode,
|
|
203
144
|
}),
|
|
204
145
|
}
|
|
205
146
|
}
|
|
@@ -225,10 +166,7 @@ export async function encryptChunkFromFile(fileHandle, chunkIndex, context) {
|
|
|
225
166
|
context.plainChunkSize,
|
|
226
167
|
)
|
|
227
168
|
|
|
228
|
-
let payload = plainChunk
|
|
229
|
-
if (context.compression === 'gzip') {
|
|
230
|
-
payload = await compressPlainChunk(plainChunk)
|
|
231
|
-
}
|
|
169
|
+
let payload = await compressPlainChunk(plainChunk)
|
|
232
170
|
|
|
233
171
|
return encryptChunk(payload, context.tokenBytes, context.noncePrefix, chunkIndex)
|
|
234
172
|
}
|
package/lib/upload.js
CHANGED
|
@@ -410,7 +410,6 @@ export async function uploadFile(filePath, options) {
|
|
|
410
410
|
|
|
411
411
|
const { absolutePath, context } = await resolveUploadFile(filePath, {
|
|
412
412
|
mimeType: options.mimeType,
|
|
413
|
-
compressionMode: options.compressionMode,
|
|
414
413
|
})
|
|
415
414
|
|
|
416
415
|
if (context.originalSize <= MULTIPART_THRESHOLD_BYTES) {
|