@gentleduck/upload 0.2.6 → 0.3.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.
- package/README.md +170 -193
- package/dist/core/index.d.ts +2 -3
- package/dist/core/index.js +1 -1
- package/dist/core-BVgloTiP.js +10 -0
- package/dist/index-DCqyBVcx.d.ts +1441 -0
- package/dist/index-DsZHPmJm.d.ts +225 -0
- package/dist/index-KROXtLQh.d.ts +281 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.js +1 -1
- package/dist/react/index.d.ts +2 -51
- package/dist/react/index.js +1 -1
- package/dist/react-euPrDv24.js +1 -0
- package/dist/strategies/index.d.ts +2 -56
- package/dist/strategies/index.js +1 -1
- package/dist/strategies-CUF0s68j.js +1 -0
- package/package.json +3 -2
- package/src/{__test__ → __tests__}/emitter.test.ts +4 -4
- package/src/{__test__ → __tests__}/fingerprint.test.ts +1 -1
- package/src/{__test__ → __tests__}/guards.test.ts +15 -5
- package/src/__tests__/id.test.ts +67 -0
- package/src/__tests__/mime-sniff.test.ts +142 -0
- package/src/{__test__ → __tests__}/persistence.test.ts +43 -8
- package/src/__tests__/sanitize-filename.test.ts +109 -0
- package/src/__tests__/sleep.test.ts +34 -0
- package/src/__tests__/static-analysis.test.ts +145 -0
- package/src/{__test__ → __tests__}/store-libs.test.ts +57 -3
- package/src/__tests__/transport-fetch.test.ts +108 -0
- package/src/{__test__ → __tests__}/transport.test.ts +5 -5
- package/src/core/__tests__/errors.test.ts +152 -0
- package/src/core/client.ts +3 -0
- package/src/core/contracts/contracts.types.ts +335 -141
- package/src/core/contracts/index.ts +3 -8
- package/src/core/contracts/transport/index.ts +3 -2
- package/src/core/contracts/transport/transport.fetch.ts +203 -0
- package/src/core/contracts/transport/transport.libs.ts +7 -8
- package/src/core/contracts/transport/transport.ts +91 -83
- package/src/core/contracts/transport/transport.types.ts +71 -35
- package/src/core/engine/__tests__/config-nan-bypass.test.ts +40 -0
- package/src/core/engine/engine.libs.ts +35 -0
- package/src/core/engine/engine.types.ts +367 -241
- package/src/core/engine/index.ts +3 -8
- package/src/core/engine/reducer.ts +89 -43
- package/src/core/engine/store/handlers/add-file.ts +39 -29
- package/src/core/engine/store/handlers/cancel.ts +13 -8
- package/src/core/engine/store/handlers/clean-up.ts +11 -8
- package/src/core/engine/store/handlers/create-intent.ts +82 -27
- package/src/core/engine/store/handlers/finalize.ts +30 -13
- package/src/core/engine/store/handlers/index.ts +7 -7
- package/src/core/engine/store/handlers/pause.ts +8 -6
- package/src/core/engine/store/handlers/run-upload.ts +49 -34
- package/src/core/engine/store/index.ts +27 -21
- package/src/core/engine/store/store.dispatch.ts +10 -8
- package/src/core/engine/store/store.libs.ts +184 -41
- package/src/core/engine/store/store.runtime.ts +45 -36
- package/src/core/engine/store/store.schedule.ts +27 -21
- package/src/core/engine/store/store.types.ts +177 -227
- package/src/core/engine/validation/__tests__/intent.test.ts +70 -0
- package/src/core/engine/validation/file.ts +7 -3
- package/src/core/engine/validation/index.ts +1 -0
- package/src/core/engine/validation/intent.ts +49 -27
- package/src/core/engine/validation/list.ts +14 -15
- package/src/core/engine/validation/mime.ts +39 -0
- package/src/core/errors.ts +271 -0
- package/src/core/index.ts +28 -5
- package/src/core/persistence/__tests__/deserialize-nan-bypass.test.ts +100 -0
- package/src/core/persistence/__tests__/persistence.test.ts +85 -0
- package/src/core/persistence/adapters.indexeddb.ts +9 -3
- package/src/core/persistence/adapters.local.ts +9 -3
- package/src/core/persistence/adapters.memory.ts +35 -13
- package/src/core/persistence/index.ts +5 -5
- package/src/core/persistence/persistence.ts +73 -60
- package/src/core/persistence/persistence.types.ts +140 -60
- package/src/core/utils/__tests__/guards.test.ts +93 -0
- package/src/core/utils/__tests__/url-safety.test.ts +112 -0
- package/src/core/utils/async.ts +7 -2
- package/src/core/utils/constants.ts +3 -9
- package/src/core/utils/emitter/emitter.ts +7 -24
- package/src/core/utils/emitter/emitter.types.ts +33 -16
- package/src/core/utils/emitter/index.ts +2 -74
- package/src/core/utils/fingerprint.ts +16 -5
- package/src/core/utils/guards.ts +91 -1
- package/src/core/utils/hash/__tests__/sha256.test.ts +61 -0
- package/src/core/utils/hash/index.ts +126 -0
- package/src/core/utils/hash/sha256.ts +179 -0
- package/src/core/utils/id.ts +33 -1
- package/src/core/utils/index.ts +15 -6
- package/src/core/utils/mime-sniff.ts +119 -0
- package/src/core/utils/sanitize-filename.ts +85 -0
- package/src/core/utils/url-safety.ts +156 -0
- package/src/react/__tests__/use-dropzone.test.ts +64 -0
- package/src/react/index.ts +5 -2
- package/src/react/upload-provider.tsx +68 -53
- package/src/react/uploader.types.ts +58 -61
- package/src/react/use-dropzone.tsx +226 -0
- package/src/react/use-uploader.tsx +98 -91
- package/src/strategies/_shared/__tests__/retry.test.ts +120 -0
- package/src/strategies/_shared/retry.ts +79 -0
- package/src/strategies/index.ts +9 -15
- package/src/strategies/multipart/__tests__/multipart.test.ts +229 -0
- package/src/strategies/multipart/index.ts +206 -108
- package/src/strategies/post/__tests__/post-url-safety.test.ts +119 -0
- package/src/strategies/post/index.ts +115 -33
- package/src/strategies/put/__tests__/put.test.ts +132 -0
- package/src/strategies/put/index.ts +139 -0
- package/src/strategies/registry/index.ts +12 -9
- package/src/strategies/tus/__tests__/tus.test.ts +159 -0
- package/src/strategies/tus/index.ts +211 -0
- package/dist/async-DxRWu4le.js +0 -1
- package/dist/core-CMkrP9DX.js +0 -2
- package/dist/index-C38Jmizu.d.ts +0 -1005
- package/dist/strategy.types-gg9ZStRe.d.ts +0 -225
- package/src/__test__/effect-pool.test.ts +0 -5
- package/src/__test__/id.test.ts +0 -14
- package/src/__test__/store-integration.test.ts +0 -5
- package/src/core/client/client.ts +0 -50
- package/src/core/client/client.types.ts +0 -184
- package/src/core/client/config.types.ts +0 -77
- package/src/core/client/index.ts +0 -4
- package/src/core/client/plugins.types.ts +0 -46
- package/src/core/client/retry.types.ts +0 -6
- package/src/core/client/uploadClient.ts +0 -19
- package/src/core/contracts/backendApi.ts +0 -62
- package/src/core/contracts/cursor.types.ts +0 -23
- package/src/core/contracts/errors.types.ts +0 -67
- package/src/core/contracts/fingerprint.types.ts +0 -18
- package/src/core/contracts/intent.types.ts +0 -39
- package/src/core/contracts/result.types.ts +0 -11
- package/src/core/contracts/strategy.types.ts +0 -45
- package/src/core/engine/commands.types.ts +0 -17
- package/src/core/engine/event-map.types.ts +0 -45
- package/src/core/engine/internal-events.types.ts +0 -202
- package/src/core/engine/outcome.types.ts +0 -9
- package/src/core/engine/progress.types.ts +0 -15
- package/src/core/engine/store/handlers.ts +0 -819
- package/src/core/engine/validation.ts +0 -216
- /package/src/{__test__ → __tests__}/transport-libs.test.ts +0 -0
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import type { Client } from '../client'
|
|
2
|
-
import type { Contracts } from '../contracts'
|
|
3
|
-
import { isRecord } from '../utils/guards'
|
|
4
|
-
|
|
5
|
-
// ============================================================================
|
|
6
|
-
// validateFile -- single-file rule check
|
|
7
|
-
// ============================================================================
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Validate one file against `config.validation[purpose]` rules.
|
|
11
|
-
*
|
|
12
|
-
* Empty files are always rejected.
|
|
13
|
-
*
|
|
14
|
-
* @param file File to validate.
|
|
15
|
-
* @param purpose Purpose key used to look up rules.
|
|
16
|
-
* @param config Resolved upload config.
|
|
17
|
-
* @template P Purpose discriminator string.
|
|
18
|
-
* @returns `null` when valid, otherwise a {@link Contracts.RejectReason}.
|
|
19
|
-
* @author wildduck2 <https://github.com/wildduck2>
|
|
20
|
-
*/
|
|
21
|
-
export function validateFile<P extends string>(
|
|
22
|
-
file: File,
|
|
23
|
-
purpose: P,
|
|
24
|
-
config: Client.IUploadConfig<P>,
|
|
25
|
-
): Contracts.RejectReason | null {
|
|
26
|
-
if (file.size === 0) return { code: 'empty_file' }
|
|
27
|
-
|
|
28
|
-
const rules = config.validation?.[purpose]
|
|
29
|
-
if (!rules) return null
|
|
30
|
-
|
|
31
|
-
if (rules.maxSizeBytes !== undefined && file.size > rules.maxSizeBytes) {
|
|
32
|
-
return { code: 'file_too_large', maxBytes: rules.maxSizeBytes, size: file.size }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (rules.minSizeBytes !== undefined && file.size < rules.minSizeBytes) {
|
|
36
|
-
return { code: 'empty_file' }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (rules.allowedTypes || rules.allowedExtensions) {
|
|
40
|
-
const allowed = rules.allowedTypes || []
|
|
41
|
-
const extensions = rules.allowedExtensions || []
|
|
42
|
-
|
|
43
|
-
const fileExt = file.name.split('.').pop()?.toLowerCase()
|
|
44
|
-
const mimeType = (file.type || '').toLowerCase()
|
|
45
|
-
|
|
46
|
-
const typeMatches =
|
|
47
|
-
allowed.length === 0
|
|
48
|
-
? false
|
|
49
|
-
: allowed.some((type) => {
|
|
50
|
-
if (type.endsWith('/*')) {
|
|
51
|
-
const prefix = type.slice(0, -2).toLowerCase()
|
|
52
|
-
return mimeType.startsWith(`${prefix}/`)
|
|
53
|
-
}
|
|
54
|
-
return mimeType === type.toLowerCase()
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
const extMatches =
|
|
58
|
-
fileExt && extensions.length > 0 ? extensions.some((ext) => ext.toLowerCase() === fileExt) : false
|
|
59
|
-
|
|
60
|
-
const hasTypeRules = allowed.length > 0
|
|
61
|
-
const hasExtRules = extensions.length > 0
|
|
62
|
-
|
|
63
|
-
// When both rule kinds are present, either match is enough; otherwise the
|
|
64
|
-
// present one must match.
|
|
65
|
-
const ok = (hasTypeRules ? typeMatches : true) && (hasExtRules ? extMatches : true)
|
|
66
|
-
const okEither = hasTypeRules && hasExtRules ? typeMatches || extMatches : ok
|
|
67
|
-
|
|
68
|
-
if (!okEither) {
|
|
69
|
-
return {
|
|
70
|
-
code: 'type_not_allowed',
|
|
71
|
-
allowed: [...allowed, ...extensions],
|
|
72
|
-
got: file.type || file.name,
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return null
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ============================================================================
|
|
81
|
-
// validateFileList -- batch validation with maxFiles cap
|
|
82
|
-
// ============================================================================
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Validate a batch of files and split into accepted vs rejected.
|
|
86
|
-
*
|
|
87
|
-
* Enforces `maxFiles` against the existing count plus the running additions
|
|
88
|
-
* so a single `addFiles` call cannot overshoot.
|
|
89
|
-
*
|
|
90
|
-
* @param files Files to validate.
|
|
91
|
-
* @param purpose Purpose key used to look up rules.
|
|
92
|
-
* @param config Resolved upload config.
|
|
93
|
-
* @param existingCount Items already in the store for this purpose.
|
|
94
|
-
* @template P Purpose discriminator string.
|
|
95
|
-
* @returns Object with `valid` files and `rejected` files plus reasons.
|
|
96
|
-
* @author wildduck2 <https://github.com/wildduck2>
|
|
97
|
-
*/
|
|
98
|
-
export function validateFileList<P extends string>(
|
|
99
|
-
files: File[],
|
|
100
|
-
purpose: P,
|
|
101
|
-
config: Client.IUploadConfig<P>,
|
|
102
|
-
existingCount: number = 0,
|
|
103
|
-
): { valid: File[]; rejected: Array<{ file: File; reason: Contracts.RejectReason }> } {
|
|
104
|
-
const rules = config.validation?.[purpose]
|
|
105
|
-
const valid: File[] = []
|
|
106
|
-
const rejected: Array<{ file: File; reason: Contracts.RejectReason }> = []
|
|
107
|
-
|
|
108
|
-
const maxFiles = rules?.maxFiles
|
|
109
|
-
let remaining = maxFiles !== undefined ? Math.max(0, maxFiles - existingCount) : Number.POSITIVE_INFINITY
|
|
110
|
-
|
|
111
|
-
for (const file of files) {
|
|
112
|
-
if (remaining <= 0) {
|
|
113
|
-
const limit = maxFiles ?? 0
|
|
114
|
-
rejected.push({ file, reason: { code: 'too_many_files', max: limit } })
|
|
115
|
-
continue
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const reason = validateFile(file, purpose, config)
|
|
119
|
-
if (reason) {
|
|
120
|
-
rejected.push({ file, reason })
|
|
121
|
-
continue
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
valid.push(file)
|
|
125
|
-
remaining -= 1
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return { valid, rejected }
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ============================================================================
|
|
132
|
-
// validateIntent -- backend-intent shape check
|
|
133
|
-
// ============================================================================
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Validate an intent payload returned by the backend before the engine
|
|
137
|
-
* commits to it.
|
|
138
|
-
*
|
|
139
|
-
* Rejects malformed shapes, protocol-foreign URLs, and cross-field
|
|
140
|
-
* inconsistencies (e.g. `partCount * partSize` cannot cover the file).
|
|
141
|
-
*
|
|
142
|
-
* @param intent Raw intent value from `createIntent`.
|
|
143
|
-
* @param strategy Expected `strategy` discriminator.
|
|
144
|
-
* @param fileSize File byte length; enables multipart cross-checks.
|
|
145
|
-
* @returns `Error` describing the failure, or `null` if valid.
|
|
146
|
-
* @author wildduck2 <https://github.com/wildduck2>
|
|
147
|
-
*/
|
|
148
|
-
export function validateIntent(intent: unknown, strategy: string, fileSize?: number): Error | null {
|
|
149
|
-
if (!isRecord(intent)) {
|
|
150
|
-
return new Error('Invalid intent: must be an object')
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (typeof intent.strategy !== 'string') {
|
|
154
|
-
return new Error('Invalid intent: missing or invalid strategy field')
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (intent.strategy !== strategy) {
|
|
158
|
-
return new Error(`Invalid intent: strategy mismatch (expected ${strategy}, got ${intent.strategy})`)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (typeof intent.fileId !== 'string' || !intent.fileId) {
|
|
162
|
-
return new Error('Invalid intent: missing or invalid fileId')
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (strategy === 'post') {
|
|
166
|
-
if (typeof intent.url !== 'string' || !intent.url) {
|
|
167
|
-
return new Error('Invalid post intent: missing or invalid url')
|
|
168
|
-
}
|
|
169
|
-
try {
|
|
170
|
-
const url = new URL(intent.url)
|
|
171
|
-
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
172
|
-
return new Error('Invalid post intent: url must use http or https protocol')
|
|
173
|
-
}
|
|
174
|
-
} catch {
|
|
175
|
-
return new Error('Invalid post intent: url is not a valid URL')
|
|
176
|
-
}
|
|
177
|
-
if (!intent.fields || typeof intent.fields !== 'object') {
|
|
178
|
-
return new Error('Invalid post intent: missing or invalid fields')
|
|
179
|
-
}
|
|
180
|
-
} else if (strategy === 'multipart') {
|
|
181
|
-
if (typeof intent.uploadId !== 'string' || !intent.uploadId) {
|
|
182
|
-
return new Error('Invalid multipart intent: missing or invalid uploadId')
|
|
183
|
-
}
|
|
184
|
-
if (typeof intent.partSize !== 'number' || intent.partSize <= 0) {
|
|
185
|
-
return new Error('Invalid multipart intent: missing or invalid partSize')
|
|
186
|
-
}
|
|
187
|
-
if ('partCount' in intent && intent.partCount !== undefined) {
|
|
188
|
-
if (typeof intent.partCount !== 'number' || !Number.isFinite(intent.partCount) || intent.partCount <= 0) {
|
|
189
|
-
return new Error('Invalid multipart intent: partCount must be a positive number')
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
if (typeof fileSize === 'number' && typeof intent.partCount === 'number' && intent.partCount > 0) {
|
|
193
|
-
const partSize = intent.partSize
|
|
194
|
-
const partCount = intent.partCount
|
|
195
|
-
const maxBytes = partCount * partSize
|
|
196
|
-
const minBytes = (partCount - 1) * partSize
|
|
197
|
-
if (maxBytes < fileSize) {
|
|
198
|
-
return new Error(
|
|
199
|
-
`Invalid multipart intent: partCount * partSize (${maxBytes}) is smaller than file size (${fileSize})`,
|
|
200
|
-
)
|
|
201
|
-
}
|
|
202
|
-
if (minBytes >= fileSize && partCount > 1) {
|
|
203
|
-
return new Error(
|
|
204
|
-
`Invalid multipart intent: partCount (${partCount}) exceeds what file size (${fileSize}) needs at partSize ${partSize}`,
|
|
205
|
-
)
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
if ('parts' in intent && intent.parts !== undefined) {
|
|
209
|
-
if (!Array.isArray(intent.parts)) {
|
|
210
|
-
return new Error('Invalid multipart intent: parts must be an array if provided')
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return null
|
|
216
|
-
}
|
|
File without changes
|