@uploadista/expo 0.0.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/LICENSE +21 -0
- package/README.md +567 -0
- package/expo-env.d.ts +3 -0
- package/package.json +45 -0
- package/src/client/create-uploadista-client.ts +67 -0
- package/src/client/index.ts +4 -0
- package/src/index.ts +77 -0
- package/src/services/abort-controller-factory.ts +32 -0
- package/src/services/base64-service.ts +29 -0
- package/src/services/checksum-service.ts +14 -0
- package/src/services/create-expo-services.ts +85 -0
- package/src/services/expo-file-system-provider.ts +227 -0
- package/src/services/file-reader-service.ts +184 -0
- package/src/services/fingerprint-service.ts +107 -0
- package/src/services/http-client.ts +235 -0
- package/src/services/id-generation-service.ts +14 -0
- package/src/services/index.ts +13 -0
- package/src/services/platform-service.ts +71 -0
- package/src/services/storage-service.ts +62 -0
- package/src/services/websocket-factory.ts +62 -0
- package/src/types/upload-input.ts +5 -0
- package/src/types.ts +116 -0
- package/src/utils/hash-util.ts +70 -0
- package/tsconfig.json +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 uploadista
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
# @uploadista/expo
|
|
2
|
+
|
|
3
|
+
Expo client for Uploadista - file uploads from mobile devices with Expo.
|
|
4
|
+
|
|
5
|
+
Provides Expo-specific implementations for file uploads, including:
|
|
6
|
+
- Direct file system access (async storage)
|
|
7
|
+
- Image picker and camera integration
|
|
8
|
+
- Optimized chunked uploads for mobile networks
|
|
9
|
+
- Resumable uploads with automatic retry
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Expo Native APIs** - Uses Expo's DocumentPicker, ImagePicker, MediaLibrary
|
|
14
|
+
- **Automatic Resumption** - Resume failed uploads without re-uploading
|
|
15
|
+
- **Chunked Uploads** - Configurable chunk sizes for reliable transfers
|
|
16
|
+
- **Mobile-Optimized** - Handles network interruptions gracefully
|
|
17
|
+
- **TypeScript Support** - Full type safety for all APIs
|
|
18
|
+
- **Storage Integration** - AsyncStorage for upload state persistence
|
|
19
|
+
- **Camera Support** - Direct camera capture and upload
|
|
20
|
+
- **Image Library** - Pick and upload images from device library
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pnpm add @uploadista/expo expo expo-document-picker expo-image-picker
|
|
26
|
+
# or
|
|
27
|
+
npm install @uploadista/expo expo expo-document-picker expo-image-picker
|
|
28
|
+
# or
|
|
29
|
+
yarn add @uploadista/expo expo expo-document-picker expo-image-picker
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- React Native 0.60+
|
|
35
|
+
- Expo 46+
|
|
36
|
+
- iOS 11+ or Android 6+
|
|
37
|
+
- TypeScript 5.0+ (optional but recommended)
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
### 1. Create Uploadista Client
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { createUploadistaClient } from '@uploadista/expo'
|
|
45
|
+
|
|
46
|
+
const client = createUploadistaClient({
|
|
47
|
+
baseUrl: 'https://api.example.com',
|
|
48
|
+
storageId: 'my-storage',
|
|
49
|
+
chunkSize: 1024 * 1024, // 1MB chunks
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 2. Pick and Upload File from Device
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { useUpload } from '@uploadista/expo'
|
|
57
|
+
import * as DocumentPicker from 'expo-document-picker'
|
|
58
|
+
|
|
59
|
+
export function FileUploadScreen() {
|
|
60
|
+
const { state, upload } = useUpload()
|
|
61
|
+
|
|
62
|
+
const handlePickFile = async () => {
|
|
63
|
+
const result = await DocumentPicker.getDocumentAsync()
|
|
64
|
+
if (result.type === 'success') {
|
|
65
|
+
await upload(result)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<View>
|
|
71
|
+
<TouchableOpacity onPress={handlePickFile}>
|
|
72
|
+
<Text>Pick File</Text>
|
|
73
|
+
</TouchableOpacity>
|
|
74
|
+
|
|
75
|
+
{state.status === 'uploading' && (
|
|
76
|
+
<Text>Progress: {Math.round(state.progress)}%</Text>
|
|
77
|
+
)}
|
|
78
|
+
{state.status === 'success' && <Text>Upload Complete!</Text>}
|
|
79
|
+
{state.status === 'error' && <Text>Error: {state.error?.message}</Text>}
|
|
80
|
+
</View>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 3. Upload from Camera
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { useUpload } from '@uploadista/expo'
|
|
89
|
+
import * as ImagePicker from 'expo-image-picker'
|
|
90
|
+
|
|
91
|
+
export function CameraUploadScreen() {
|
|
92
|
+
const { state, upload } = useUpload()
|
|
93
|
+
|
|
94
|
+
const handleTakePhoto = async () => {
|
|
95
|
+
const result = await ImagePicker.launchCameraAsync({
|
|
96
|
+
mediaTypes: 'Images',
|
|
97
|
+
quality: 0.8,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
if (!result.canceled) {
|
|
101
|
+
await upload(result.assets[0])
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<View>
|
|
107
|
+
<TouchableOpacity onPress={handleTakePhoto}>
|
|
108
|
+
<Text>Take Photo</Text>
|
|
109
|
+
</TouchableOpacity>
|
|
110
|
+
|
|
111
|
+
{state.status === 'uploading' && (
|
|
112
|
+
<ProgressBar value={state.progress} max={100} />
|
|
113
|
+
)}
|
|
114
|
+
{state.status === 'success' && (
|
|
115
|
+
<Text>Photo uploaded: {state.result?.filename}</Text>
|
|
116
|
+
)}
|
|
117
|
+
</View>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 4. Upload from Image Library
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { useUpload } from '@uploadista/expo'
|
|
126
|
+
import * as ImagePicker from 'expo-image-picker'
|
|
127
|
+
|
|
128
|
+
export function ImageLibraryUploadScreen() {
|
|
129
|
+
const { state, upload } = useUpload()
|
|
130
|
+
|
|
131
|
+
const handlePickImage = async () => {
|
|
132
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
133
|
+
mediaTypes: 'Images',
|
|
134
|
+
multiple: false,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
if (!result.canceled) {
|
|
138
|
+
await upload(result.assets[0])
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<View>
|
|
144
|
+
<TouchableOpacity onPress={handlePickImage}>
|
|
145
|
+
<Text>Pick from Library</Text>
|
|
146
|
+
</TouchableOpacity>
|
|
147
|
+
|
|
148
|
+
{state.status === 'uploading' && (
|
|
149
|
+
<Text>Uploading: {state.bytesUploaded} / {state.totalBytes} bytes</Text>
|
|
150
|
+
)}
|
|
151
|
+
{state.status === 'success' && (
|
|
152
|
+
<Image source={{ uri: state.result?.filename }} />
|
|
153
|
+
)}
|
|
154
|
+
</View>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## API Reference
|
|
160
|
+
|
|
161
|
+
### Client
|
|
162
|
+
|
|
163
|
+
#### `createUploadistaClient(options)`
|
|
164
|
+
|
|
165
|
+
Creates the Expo Uploadista client.
|
|
166
|
+
|
|
167
|
+
**Options:**
|
|
168
|
+
- `baseUrl` (string) - API base URL
|
|
169
|
+
- `storageId` (string) - Storage backend identifier
|
|
170
|
+
- `chunkSize` (number, optional) - Chunk size in bytes (default: 1MB)
|
|
171
|
+
- `concurrency` (number, optional) - Concurrent chunk uploads (default: 3)
|
|
172
|
+
- `maxRetries` (number, optional) - Max retries per chunk (default: 3)
|
|
173
|
+
- `timeout` (number, optional) - Request timeout in ms
|
|
174
|
+
|
|
175
|
+
**Returns:** UploadistaClient instance
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
const client = createUploadistaClient({
|
|
179
|
+
baseUrl: 'https://api.example.com',
|
|
180
|
+
storageId: 'my-storage',
|
|
181
|
+
chunkSize: 2 * 1024 * 1024, // 2MB chunks
|
|
182
|
+
concurrency: 4,
|
|
183
|
+
maxRetries: 5,
|
|
184
|
+
timeout: 30000,
|
|
185
|
+
})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Composables
|
|
189
|
+
|
|
190
|
+
#### `useUpload(options?)`
|
|
191
|
+
|
|
192
|
+
Single file upload composable.
|
|
193
|
+
|
|
194
|
+
**Returns:**
|
|
195
|
+
- `state` - Upload state (readonly)
|
|
196
|
+
- `status` - 'idle' | 'uploading' | 'success' | 'error' | 'aborted'
|
|
197
|
+
- `progress` - Progress 0-100
|
|
198
|
+
- `bytesUploaded` - Bytes uploaded
|
|
199
|
+
- `totalBytes` - Total file size
|
|
200
|
+
- `result` - Upload result
|
|
201
|
+
- `error` - Error object
|
|
202
|
+
- `upload(file, options?)` - Upload file
|
|
203
|
+
- `abort()` - Cancel upload
|
|
204
|
+
- `reset()` - Reset to idle
|
|
205
|
+
- `retry()` - Retry failed upload
|
|
206
|
+
|
|
207
|
+
**Options:**
|
|
208
|
+
- `onProgress(event)` - Progress callback
|
|
209
|
+
- `onComplete(result)` - Success callback
|
|
210
|
+
- `onError(error)` - Error callback
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
const { state, upload, abort } = useUpload({
|
|
214
|
+
onProgress: (event) => console.log(event.progress + '%'),
|
|
215
|
+
onComplete: (result) => console.log('Uploaded:', result.filename),
|
|
216
|
+
onError: (error) => console.error('Upload failed:', error),
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
// Upload file
|
|
220
|
+
await upload(file)
|
|
221
|
+
|
|
222
|
+
// Cancel
|
|
223
|
+
abort()
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
#### `useMultiUpload(options?)`
|
|
227
|
+
|
|
228
|
+
Multiple concurrent file uploads.
|
|
229
|
+
|
|
230
|
+
**Returns:**
|
|
231
|
+
- `uploads` - Array of upload items
|
|
232
|
+
- `stats` - Aggregate statistics
|
|
233
|
+
- `totalFiles` - Total files
|
|
234
|
+
- `completedFiles` - Successfully uploaded
|
|
235
|
+
- `failedFiles` - Failed uploads
|
|
236
|
+
- `totalBytes` - Total size
|
|
237
|
+
- `uploadedBytes` - Bytes uploaded
|
|
238
|
+
- `totalProgress` - Overall progress 0-100
|
|
239
|
+
- `allComplete` - All finished
|
|
240
|
+
- `hasErrors` - Any failures
|
|
241
|
+
- `add(files)` - Add files to queue
|
|
242
|
+
- `remove(uploadId)` - Remove upload
|
|
243
|
+
- `clear()` - Clear all
|
|
244
|
+
- `retryFailed()` - Retry failures
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
const { uploads, stats, add, retryFailed } = useMultiUpload()
|
|
248
|
+
|
|
249
|
+
// Add files
|
|
250
|
+
await add([file1, file2, file3])
|
|
251
|
+
|
|
252
|
+
// Monitor progress
|
|
253
|
+
console.log(`${stats.value.uploadedBytes} / ${stats.value.totalBytes}`)
|
|
254
|
+
|
|
255
|
+
// Retry on failure
|
|
256
|
+
if (stats.value.hasErrors) {
|
|
257
|
+
await retryFailed()
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Services
|
|
262
|
+
|
|
263
|
+
#### `createExpoServices(options?)`
|
|
264
|
+
|
|
265
|
+
Creates all Expo-specific services for the client.
|
|
266
|
+
|
|
267
|
+
**Returns:** Service container with implementations for:
|
|
268
|
+
- `fileReaderService` - Read file chunks
|
|
269
|
+
- `httpClient` - HTTP requests
|
|
270
|
+
- `storageService` - AsyncStorage persistence
|
|
271
|
+
- `base64Service` - Base64 encoding
|
|
272
|
+
- `idGenerationService` - ID generation
|
|
273
|
+
- `checksumService` - File hashing
|
|
274
|
+
- `websocketFactory` - WebSocket creation
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
import { createExpoServices } from '@uploadista/expo'
|
|
278
|
+
import { createUploadistaClientCore } from '@uploadista/client-core'
|
|
279
|
+
|
|
280
|
+
const services = createExpoServices({
|
|
281
|
+
asyncStorageKey: '@uploadista/uploads',
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
const client = createUploadistaClientCore({
|
|
285
|
+
endpoint: 'https://api.example.com',
|
|
286
|
+
services,
|
|
287
|
+
})
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
#### File System Provider (Legacy)
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { ExpoFileSystemProvider } from '@uploadista/expo'
|
|
294
|
+
|
|
295
|
+
const provider = new ExpoFileSystemProvider()
|
|
296
|
+
|
|
297
|
+
// Pick image from camera
|
|
298
|
+
const photoResult = await provider.pickImage({ camera: true })
|
|
299
|
+
|
|
300
|
+
// Pick from library
|
|
301
|
+
const libraryResult = await provider.pickImage({ camera: false })
|
|
302
|
+
|
|
303
|
+
// Pick document
|
|
304
|
+
const documentResult = await provider.pickDocument()
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Configuration
|
|
308
|
+
|
|
309
|
+
### Permissions
|
|
310
|
+
|
|
311
|
+
Add required permissions to `app.json`:
|
|
312
|
+
|
|
313
|
+
```json
|
|
314
|
+
{
|
|
315
|
+
"expo": {
|
|
316
|
+
"plugins": [
|
|
317
|
+
[
|
|
318
|
+
"expo-image-picker",
|
|
319
|
+
{
|
|
320
|
+
"photosPermission": "Allow $(PRODUCT_NAME) to access photos.",
|
|
321
|
+
"cameraPermission": "Allow $(PRODUCT_NAME) to access camera."
|
|
322
|
+
}
|
|
323
|
+
],
|
|
324
|
+
[
|
|
325
|
+
"expo-document-picker",
|
|
326
|
+
{
|
|
327
|
+
"iCloudPermission": "Allow $(PRODUCT_NAME) to access iCloud."
|
|
328
|
+
}
|
|
329
|
+
]
|
|
330
|
+
]
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Chunk Size Configuration
|
|
336
|
+
|
|
337
|
+
Choose chunk sizes based on network conditions:
|
|
338
|
+
|
|
339
|
+
**Fast Networks (WiFi):**
|
|
340
|
+
```typescript
|
|
341
|
+
const client = createUploadistaClient({
|
|
342
|
+
chunkSize: 5 * 1024 * 1024, // 5MB chunks
|
|
343
|
+
concurrency: 6,
|
|
344
|
+
})
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Mobile Networks (LTE/4G):**
|
|
348
|
+
```typescript
|
|
349
|
+
const client = createUploadistaClient({
|
|
350
|
+
chunkSize: 2 * 1024 * 1024, // 2MB chunks
|
|
351
|
+
concurrency: 3,
|
|
352
|
+
})
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
**Slow Networks (3G/Edge):**
|
|
356
|
+
```typescript
|
|
357
|
+
const client = createUploadistaClient({
|
|
358
|
+
chunkSize: 512 * 1024, // 512KB chunks
|
|
359
|
+
concurrency: 2,
|
|
360
|
+
})
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Examples
|
|
364
|
+
|
|
365
|
+
### Complete Upload Screen
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
import { useUpload } from '@uploadista/expo'
|
|
369
|
+
import * as ImagePicker from 'expo-image-picker'
|
|
370
|
+
import {
|
|
371
|
+
View,
|
|
372
|
+
TouchableOpacity,
|
|
373
|
+
Text,
|
|
374
|
+
Image,
|
|
375
|
+
ProgressBarAndroidBase,
|
|
376
|
+
} from 'react-native'
|
|
377
|
+
|
|
378
|
+
export function UploadPhotoScreen() {
|
|
379
|
+
const { state, upload, abort } = useUpload()
|
|
380
|
+
|
|
381
|
+
const pickAndUpload = async () => {
|
|
382
|
+
// Request permissions
|
|
383
|
+
const { granted } = await ImagePicker.requestMediaLibraryPermissionsAsync()
|
|
384
|
+
if (!granted) return
|
|
385
|
+
|
|
386
|
+
// Pick image
|
|
387
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
388
|
+
mediaTypes: 'Images',
|
|
389
|
+
quality: 0.8,
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
if (!result.canceled) {
|
|
393
|
+
// Upload
|
|
394
|
+
await upload(result.assets[0])
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return (
|
|
399
|
+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
|
400
|
+
{state.status === 'idle' && (
|
|
401
|
+
<TouchableOpacity onPress={pickAndUpload}>
|
|
402
|
+
<Text style={{ fontSize: 18 }}>Pick and Upload Photo</Text>
|
|
403
|
+
</TouchableOpacity>
|
|
404
|
+
)}
|
|
405
|
+
|
|
406
|
+
{state.status === 'uploading' && (
|
|
407
|
+
<View style={{ width: '80%' }}>
|
|
408
|
+
<ProgressBarAndroidBase
|
|
409
|
+
styleAttr="Horizontal"
|
|
410
|
+
indeterminate={false}
|
|
411
|
+
progress={state.progress / 100}
|
|
412
|
+
/>
|
|
413
|
+
<Text>
|
|
414
|
+
{Math.round(state.progress)}% - {Math.round(state.bytesUploaded / 1024 / 1024)}MB /
|
|
415
|
+
{Math.round(state.totalBytes! / 1024 / 1024)}MB
|
|
416
|
+
</Text>
|
|
417
|
+
<TouchableOpacity onPress={abort}>
|
|
418
|
+
<Text style={{ color: 'red' }}>Cancel</Text>
|
|
419
|
+
</TouchableOpacity>
|
|
420
|
+
</View>
|
|
421
|
+
)}
|
|
422
|
+
|
|
423
|
+
{state.status === 'success' && (
|
|
424
|
+
<View>
|
|
425
|
+
<Image source={{ uri: state.result?.filename }} style={{ width: 200, height: 200 }} />
|
|
426
|
+
<Text>Upload successful!</Text>
|
|
427
|
+
<Text>File: {state.result?.filename}</Text>
|
|
428
|
+
</View>
|
|
429
|
+
)}
|
|
430
|
+
|
|
431
|
+
{state.status === 'error' && (
|
|
432
|
+
<View>
|
|
433
|
+
<Text style={{ color: 'red' }}>Upload failed</Text>
|
|
434
|
+
<Text>{state.error?.message}</Text>
|
|
435
|
+
<TouchableOpacity onPress={() => upload(/* file */)}>
|
|
436
|
+
<Text>Retry</Text>
|
|
437
|
+
</TouchableOpacity>
|
|
438
|
+
</View>
|
|
439
|
+
)}
|
|
440
|
+
</View>
|
|
441
|
+
)
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Multiple Files Upload
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
import { useMultiUpload } from '@uploadista/expo'
|
|
449
|
+
import * as ImagePicker from 'expo-image-picker'
|
|
450
|
+
import { FlatList, View } from 'react-native'
|
|
451
|
+
|
|
452
|
+
export function MultiUploadScreen() {
|
|
453
|
+
const { uploads, stats, add } = useMultiUpload()
|
|
454
|
+
|
|
455
|
+
const pickMultiple = async () => {
|
|
456
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
457
|
+
mediaTypes: 'Images',
|
|
458
|
+
multiple: true,
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
if (!result.canceled) {
|
|
462
|
+
await add(result.assets)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return (
|
|
467
|
+
<View>
|
|
468
|
+
<Button title="Add Photos" onPress={pickMultiple} />
|
|
469
|
+
|
|
470
|
+
<Text>
|
|
471
|
+
{stats.value.completedFiles} / {stats.value.totalFiles} uploaded
|
|
472
|
+
</Text>
|
|
473
|
+
|
|
474
|
+
<FlatList
|
|
475
|
+
data={uploads}
|
|
476
|
+
keyExtractor={(item) => item.id}
|
|
477
|
+
renderItem={({ item }) => (
|
|
478
|
+
<View>
|
|
479
|
+
<Text>{item.filename}</Text>
|
|
480
|
+
<Text>{Math.round(item.progress)}%</Text>
|
|
481
|
+
<Text>{item.state.status}</Text>
|
|
482
|
+
</View>
|
|
483
|
+
)}
|
|
484
|
+
/>
|
|
485
|
+
</View>
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
## Troubleshooting
|
|
491
|
+
|
|
492
|
+
### Permission Denied Errors
|
|
493
|
+
|
|
494
|
+
Ensure permissions are requested before accessing files:
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
const { granted } = await ImagePicker.requestMediaLibraryPermissionsAsync()
|
|
498
|
+
if (!granted) {
|
|
499
|
+
alert('Permission to access media library is required')
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Network Interruptions
|
|
504
|
+
|
|
505
|
+
Uploads automatically resume from the last successful chunk. To manually retry:
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
const { state, retry } = useUpload()
|
|
509
|
+
|
|
510
|
+
if (state.value.status === 'error') {
|
|
511
|
+
await retry()
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### Memory Issues with Large Files
|
|
516
|
+
|
|
517
|
+
Use smaller chunk sizes for large files:
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
const client = createUploadistaClient({
|
|
521
|
+
chunkSize: 512 * 1024, // Smaller chunks use less memory
|
|
522
|
+
concurrency: 1,
|
|
523
|
+
})
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Slow Uploads
|
|
527
|
+
|
|
528
|
+
Increase chunk size and concurrency for faster networks:
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
const client = createUploadistaClient({
|
|
532
|
+
chunkSize: 5 * 1024 * 1024,
|
|
533
|
+
concurrency: 5,
|
|
534
|
+
})
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
## Related Packages
|
|
538
|
+
|
|
539
|
+
- **[@uploadista/client-core](../core/)** - Core client and types
|
|
540
|
+
- **[@uploadista/react-native-core](../react-native-core/)** - React Native composables
|
|
541
|
+
- **[@uploadista/client-browser](../browser/)** - Browser client implementation
|
|
542
|
+
|
|
543
|
+
## TypeScript Support
|
|
544
|
+
|
|
545
|
+
Full TypeScript support included. Type definitions for all APIs:
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
import type {
|
|
549
|
+
UploadistaClientOptions,
|
|
550
|
+
UploadState,
|
|
551
|
+
UseUploadOptions,
|
|
552
|
+
UseMultiUploadOptions,
|
|
553
|
+
ExpoServiceOptions,
|
|
554
|
+
} from '@uploadista/expo'
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
## Performance Tips
|
|
558
|
+
|
|
559
|
+
1. **Chunk Size** - Larger chunks (2-5MB) for fast networks, smaller (512KB) for slow
|
|
560
|
+
2. **Concurrency** - Balance between 2-6 concurrent chunks based on network
|
|
561
|
+
3. **Compression** - Pre-compress large files before upload (images, videos)
|
|
562
|
+
4. **Resumption** - Automatically handled; failed chunks restart without re-uploading
|
|
563
|
+
5. **Background Upload** - Consider using Background Tasks for long uploads
|
|
564
|
+
|
|
565
|
+
## License
|
|
566
|
+
|
|
567
|
+
MIT
|
package/expo-env.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uploadista/expo",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Expo client for Uploadista with managed workflow support",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Uploadista",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./services": "./src/services/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"uuid": "^10.0.0",
|
|
14
|
+
"js-base64": "^3.7.7",
|
|
15
|
+
"@uploadista/core": "0.0.3",
|
|
16
|
+
"@uploadista/client-core": "0.0.3",
|
|
17
|
+
"@uploadista/react-native-core": "0.0.3"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@react-native-async-storage/async-storage": ">=1.17.0",
|
|
21
|
+
"expo-camera": ">=15.0.0",
|
|
22
|
+
"expo-document-picker": ">=12.0.0",
|
|
23
|
+
"expo-file-system": ">=16.0.0",
|
|
24
|
+
"expo-image-picker": ">=14.0.0",
|
|
25
|
+
"expo-crypto": "15.0.7",
|
|
26
|
+
"react": ">=16.8.0",
|
|
27
|
+
"react-native": ">=0.71.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependenciesMeta": {
|
|
30
|
+
"@react-native-async-storage/async-storage": {
|
|
31
|
+
"optional": true
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/react": ">=18.0.0",
|
|
36
|
+
"@types/react-native": ">=0.71.0",
|
|
37
|
+
"@types/uuid": "^10.0.0",
|
|
38
|
+
"@uploadista/typescript-config": "0.0.3"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"format": "biome format --write ./src",
|
|
42
|
+
"lint": "biome lint --write ./src",
|
|
43
|
+
"check": "biome check --write ./src"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ConnectionPoolConfig,
|
|
3
|
+
createClientStorage,
|
|
4
|
+
createLogger,
|
|
5
|
+
createUploadistaClient as createUploadistaClientCore,
|
|
6
|
+
type UploadistaClientOptions as UploadistaClientOptionsCore,
|
|
7
|
+
} from "@uploadista/client-core";
|
|
8
|
+
import { createExpoServices } from "../services/create-expo-services";
|
|
9
|
+
import type { ExpoUploadInput } from "../types/upload-input";
|
|
10
|
+
|
|
11
|
+
export interface UploadistaClientOptions
|
|
12
|
+
extends Omit<
|
|
13
|
+
UploadistaClientOptionsCore<ExpoUploadInput>,
|
|
14
|
+
| "webSocketFactory"
|
|
15
|
+
| "abortControllerFactory"
|
|
16
|
+
| "generateId"
|
|
17
|
+
| "clientStorage"
|
|
18
|
+
| "logger"
|
|
19
|
+
| "httpClient"
|
|
20
|
+
| "fileReader"
|
|
21
|
+
| "base64"
|
|
22
|
+
> {
|
|
23
|
+
connectionPooling?: ConnectionPoolConfig;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether to use AsyncStorage for persistence
|
|
27
|
+
* If false, uses in-memory storage
|
|
28
|
+
* @default true
|
|
29
|
+
*/
|
|
30
|
+
useAsyncStorage?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Creates an upload client instance with Expo-specific service implementations
|
|
35
|
+
*
|
|
36
|
+
* @param options - Client configuration options
|
|
37
|
+
* @returns Configured UploadistaClient instance
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* import { createUploadistaClient } from '@uploadista/expo'
|
|
42
|
+
*
|
|
43
|
+
* const client = createUploadistaClient({
|
|
44
|
+
* baseUrl: 'https://api.example.com',
|
|
45
|
+
* storageId: 'my-storage',
|
|
46
|
+
* chunkSize: 1024 * 1024, // 1MB
|
|
47
|
+
* useAsyncStorage: true,
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function createUploadistaClient(options: UploadistaClientOptions) {
|
|
52
|
+
const services = createExpoServices({
|
|
53
|
+
connectionPooling: options.connectionPooling,
|
|
54
|
+
useAsyncStorage: options.useAsyncStorage,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return createUploadistaClientCore<ExpoUploadInput>({
|
|
58
|
+
...options,
|
|
59
|
+
webSocketFactory: services.websocket,
|
|
60
|
+
abortControllerFactory: services.abortController,
|
|
61
|
+
httpClient: services.httpClient,
|
|
62
|
+
fileReader: services.fileReader,
|
|
63
|
+
generateId: services.idGeneration,
|
|
64
|
+
logger: createLogger(false, () => {}),
|
|
65
|
+
clientStorage: createClientStorage(services.storage),
|
|
66
|
+
});
|
|
67
|
+
}
|