uploados 0.1.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/android/build.gradle +25 -0
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/java/expo/modules/uploados/UploadosModule.kt +121 -0
- package/android/src/main/java/expo/modules/uploados/upload/CompressionPipeline.kt +192 -0
- package/android/src/main/java/expo/modules/uploados/upload/FileStager.kt +125 -0
- package/android/src/main/java/expo/modules/uploados/upload/ProgressRequestBody.kt +36 -0
- package/android/src/main/java/expo/modules/uploados/upload/UploadManager.kt +857 -0
- package/android/src/main/java/expo/modules/uploados/upload/UploadModels.kt +209 -0
- package/android/src/main/java/expo/modules/uploados/upload/UploadNotificationHelper.kt +93 -0
- package/android/src/main/java/expo/modules/uploados/upload/UploadTaskStore.kt +224 -0
- package/android/src/main/java/expo/modules/uploados/upload/UploadWorker.kt +31 -0
- package/build/Uploados.types.d.ts +226 -0
- package/build/Uploados.types.d.ts.map +1 -0
- package/build/Uploados.types.js +2 -0
- package/build/Uploados.types.js.map +1 -0
- package/build/UploadosModule.d.ts +13 -0
- package/build/UploadosModule.d.ts.map +1 -0
- package/build/UploadosModule.js +3 -0
- package/build/UploadosModule.js.map +1 -0
- package/build/UploadosModule.web.d.ts +13 -0
- package/build/UploadosModule.web.d.ts.map +1 -0
- package/build/UploadosModule.web.js +33 -0
- package/build/UploadosModule.web.js.map +1 -0
- package/build/createUploader.d.ts +3 -0
- package/build/createUploader.d.ts.map +1 -0
- package/build/createUploader.js +108 -0
- package/build/createUploader.js.map +1 -0
- package/build/index.d.ts +5 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +5 -0
- package/build/index.js.map +1 -0
- package/build/normalizeUploadOptions.d.ts +9 -0
- package/build/normalizeUploadOptions.d.ts.map +1 -0
- package/build/normalizeUploadOptions.js +81 -0
- package/build/normalizeUploadOptions.js.map +1 -0
- package/build/providers/defineUploadProvider.d.ts +26 -0
- package/build/providers/defineUploadProvider.d.ts.map +1 -0
- package/build/providers/defineUploadProvider.js +39 -0
- package/build/providers/defineUploadProvider.js.map +1 -0
- package/build/providers/multipartPlan.d.ts +10 -0
- package/build/providers/multipartPlan.d.ts.map +1 -0
- package/build/providers/multipartPlan.js +28 -0
- package/build/providers/multipartPlan.js.map +1 -0
- package/eslint.config.cjs +5 -0
- package/expo-module.config.json +10 -0
- package/ios/Upload/CompressionPipeline.swift +183 -0
- package/ios/Upload/FileStager.swift +67 -0
- package/ios/Upload/UploadManager.swift +813 -0
- package/ios/Upload/UploadModels.swift +305 -0
- package/ios/Upload/UploadSessionDelegate.swift +82 -0
- package/ios/Upload/UploadTaskStore.swift +92 -0
- package/ios/Upload/UploadosAppDelegate.swift +14 -0
- package/ios/Uploados.podspec +23 -0
- package/ios/UploadosModule.swift +87 -0
- package/jest.config.js +15 -0
- package/package.json +54 -0
- package/readme.md +169 -0
- package/src/Uploados.types.ts +260 -0
- package/src/UploadosModule.ts +18 -0
- package/src/UploadosModule.web.ts +49 -0
- package/src/createUploader.ts +146 -0
- package/src/index.ts +4 -0
- package/src/normalizeUploadOptions.ts +132 -0
- package/src/providers/defineUploadProvider.ts +75 -0
- package/src/providers/multipartPlan.ts +43 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CompressionOptions,
|
|
3
|
+
CompressionPreset,
|
|
4
|
+
CreateUploadOptions,
|
|
5
|
+
DirectUploadOptions,
|
|
6
|
+
ProviderUploadOptions,
|
|
7
|
+
UploadErrorPayload,
|
|
8
|
+
UploadHttpMethod,
|
|
9
|
+
UploadNetworkPolicy,
|
|
10
|
+
UploaderConfig,
|
|
11
|
+
} from './Uploados.types';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
14
|
+
const DEFAULT_METHOD: UploadHttpMethod = 'PUT';
|
|
15
|
+
const DEFAULT_NETWORK_POLICY: UploadNetworkPolicy = 'wait';
|
|
16
|
+
const DEFAULT_BACKGROUND = false;
|
|
17
|
+
|
|
18
|
+
function throwValidationError(
|
|
19
|
+
message: string,
|
|
20
|
+
code: UploadErrorPayload['code'] = 'UNKNOWN'
|
|
21
|
+
): never {
|
|
22
|
+
const error: UploadErrorPayload = {
|
|
23
|
+
code,
|
|
24
|
+
message,
|
|
25
|
+
retryable: false,
|
|
26
|
+
taskId: '',
|
|
27
|
+
};
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function assertFutureUploadOptionsSupported(
|
|
32
|
+
compression?: CompressionOptions,
|
|
33
|
+
multipart?: boolean
|
|
34
|
+
): void {
|
|
35
|
+
if (compression?.enabled === true && compression.preset) {
|
|
36
|
+
const validPresets: CompressionPreset[] = ['balanced', 'inspection', 'avatar'];
|
|
37
|
+
if (!validPresets.includes(compression.preset)) {
|
|
38
|
+
throwValidationError(
|
|
39
|
+
`Unsupported compression preset "${compression.preset}". Use balanced, inspection, or avatar.`,
|
|
40
|
+
'COMPRESSION_FAILED'
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (multipart === true) {
|
|
46
|
+
throwValidationError(
|
|
47
|
+
'Multipart uploads are not available yet. Use direct signed-URL uploads until P1-E ships.',
|
|
48
|
+
'MULTIPART_FAILED'
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function normalizeDirectUploadOptions(
|
|
54
|
+
config: UploaderConfig,
|
|
55
|
+
input: DirectUploadOptions
|
|
56
|
+
): CreateUploadOptions {
|
|
57
|
+
assertFutureUploadOptionsSupported(input.compression, input.multipart);
|
|
58
|
+
|
|
59
|
+
const localUri = input.file.trim();
|
|
60
|
+
const uploadUrl = input.uploadUrl.trim();
|
|
61
|
+
if (!localUri) {
|
|
62
|
+
throwValidationError('Upload file URI is required.', 'FILE_NOT_FOUND');
|
|
63
|
+
}
|
|
64
|
+
if (!uploadUrl) {
|
|
65
|
+
throwValidationError('Upload URL is required.', 'PROVIDER_ERROR');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const background = input.background ?? config.background ?? DEFAULT_BACKGROUND;
|
|
69
|
+
const networkPolicy = background
|
|
70
|
+
? 'wait'
|
|
71
|
+
: (input.networkPolicy ?? config.networkPolicy ?? DEFAULT_NETWORK_POLICY);
|
|
72
|
+
const maxAttempts = Math.max(
|
|
73
|
+
1,
|
|
74
|
+
input.retry?.maxAttempts ?? config.retry?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
localUri,
|
|
79
|
+
uploadUrl,
|
|
80
|
+
method: input.method ?? DEFAULT_METHOD,
|
|
81
|
+
headers: input.headers ?? {},
|
|
82
|
+
background,
|
|
83
|
+
networkPolicy,
|
|
84
|
+
retry: { maxAttempts },
|
|
85
|
+
compression: input.compression,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function normalizeProviderUploadOptions(
|
|
90
|
+
config: UploaderConfig,
|
|
91
|
+
input: ProviderUploadOptions,
|
|
92
|
+
signed: {
|
|
93
|
+
uploadUrl: string;
|
|
94
|
+
method?: UploadHttpMethod;
|
|
95
|
+
headers?: Record<string, string>;
|
|
96
|
+
}
|
|
97
|
+
): CreateUploadOptions {
|
|
98
|
+
assertFutureUploadOptionsSupported(input.compression, input.multipart);
|
|
99
|
+
|
|
100
|
+
const localUri = input.file.trim();
|
|
101
|
+
const uploadUrl = signed.uploadUrl.trim();
|
|
102
|
+
const key = input.key.trim();
|
|
103
|
+
if (!localUri) {
|
|
104
|
+
throwValidationError('Upload file URI is required.', 'FILE_NOT_FOUND');
|
|
105
|
+
}
|
|
106
|
+
if (!key) {
|
|
107
|
+
throwValidationError('Upload key is required.', 'PROVIDER_ERROR');
|
|
108
|
+
}
|
|
109
|
+
if (!uploadUrl) {
|
|
110
|
+
throwValidationError('Provider did not return an upload URL.', 'PROVIDER_ERROR');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const background = input.background ?? config.background ?? DEFAULT_BACKGROUND;
|
|
114
|
+
const networkPolicy = background
|
|
115
|
+
? 'wait'
|
|
116
|
+
: (input.networkPolicy ?? config.networkPolicy ?? DEFAULT_NETWORK_POLICY);
|
|
117
|
+
const maxAttempts = Math.max(
|
|
118
|
+
1,
|
|
119
|
+
input.retry?.maxAttempts ?? config.retry?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
localUri,
|
|
124
|
+
uploadUrl,
|
|
125
|
+
method: signed.method ?? DEFAULT_METHOD,
|
|
126
|
+
headers: signed.headers ?? {},
|
|
127
|
+
background,
|
|
128
|
+
networkPolicy,
|
|
129
|
+
retry: { maxAttempts },
|
|
130
|
+
compression: input.compression,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CompleteMultipartInput,
|
|
3
|
+
CompleteMultipartResult,
|
|
4
|
+
CreateMultipartInput,
|
|
5
|
+
CreateMultipartResult,
|
|
6
|
+
SignPartInput,
|
|
7
|
+
SignedPartResult,
|
|
8
|
+
UploadHttpMethod,
|
|
9
|
+
UploadProvider,
|
|
10
|
+
} from '../Uploados.types';
|
|
11
|
+
|
|
12
|
+
export type ProviderUploadUrlInput = {
|
|
13
|
+
key: string;
|
|
14
|
+
contentType?: string;
|
|
15
|
+
metadata?: Record<string, string>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ProviderUploadUrlResult = {
|
|
19
|
+
url: string;
|
|
20
|
+
method?: UploadHttpMethod;
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type UploadProviderDefinition = {
|
|
25
|
+
getUploadUrl?: (input: ProviderUploadUrlInput) => Promise<ProviderUploadUrlResult>;
|
|
26
|
+
createMultipartUpload?: (input: CreateMultipartInput) => Promise<CreateMultipartResult>;
|
|
27
|
+
signPart?: (input: SignPartInput) => Promise<SignedPartResult>;
|
|
28
|
+
completeMultipartUpload?: (input: CompleteMultipartInput) => Promise<CompleteMultipartResult>;
|
|
29
|
+
abortMultipartUpload?: (input: { key: string; uploadId: string }) => Promise<void>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function defineUploadProvider(definition: UploadProviderDefinition): UploadProvider {
|
|
33
|
+
return {
|
|
34
|
+
getUploadUrl: definition.getUploadUrl,
|
|
35
|
+
createMultipartUpload: definition.createMultipartUpload,
|
|
36
|
+
signPart: definition.signPart,
|
|
37
|
+
completeMultipartUpload: definition.completeMultipartUpload,
|
|
38
|
+
abortMultipartUpload: definition.abortMultipartUpload,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function assertUploadProvider(provider: UploadProvider | undefined): UploadProvider {
|
|
43
|
+
if (!provider) {
|
|
44
|
+
throw providerError('Upload provider is required for provider-based uploads.');
|
|
45
|
+
}
|
|
46
|
+
return provider;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function assertProviderMultipartSupported(provider: UploadProvider): void {
|
|
50
|
+
if (typeof provider.createMultipartUpload !== 'function') {
|
|
51
|
+
throw providerError('Provider must implement createMultipartUpload.');
|
|
52
|
+
}
|
|
53
|
+
if (typeof provider.signPart !== 'function') {
|
|
54
|
+
throw providerError('Provider must implement signPart.');
|
|
55
|
+
}
|
|
56
|
+
if (typeof provider.completeMultipartUpload !== 'function') {
|
|
57
|
+
throw providerError('Provider must implement completeMultipartUpload.');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function assertProviderDirectUploadSupported(provider: UploadProvider): void {
|
|
62
|
+
if (typeof provider.getUploadUrl !== 'function') {
|
|
63
|
+
throw providerError('Provider must implement getUploadUrl for direct signed-URL uploads.');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function providerError(
|
|
68
|
+
message: string
|
|
69
|
+
): Error & { code: string; retryable: boolean; taskId: string } {
|
|
70
|
+
return Object.assign(new Error(message), {
|
|
71
|
+
code: 'PROVIDER_ERROR',
|
|
72
|
+
retryable: false,
|
|
73
|
+
taskId: '',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const DEFAULT_MULTIPART_PART_SIZE = 8 * 1024 * 1024;
|
|
2
|
+
export const MIN_MULTIPART_PART_SIZE = 5 * 1024 * 1024;
|
|
3
|
+
|
|
4
|
+
export type MultipartUploadPlan = {
|
|
5
|
+
partSize: number;
|
|
6
|
+
partCount: number;
|
|
7
|
+
totalBytes: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function createMultipartUploadPlan(
|
|
11
|
+
totalBytes: number,
|
|
12
|
+
partSize: number = DEFAULT_MULTIPART_PART_SIZE
|
|
13
|
+
): MultipartUploadPlan {
|
|
14
|
+
if (!Number.isFinite(totalBytes) || totalBytes <= 0) {
|
|
15
|
+
throw Object.assign(new Error('File size must be greater than zero for multipart uploads.'), {
|
|
16
|
+
code: 'FILE_NOT_FOUND',
|
|
17
|
+
retryable: false,
|
|
18
|
+
taskId: '',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const normalizedPartSize = Math.max(MIN_MULTIPART_PART_SIZE, Math.floor(partSize));
|
|
23
|
+
const partCount = Math.max(1, Math.ceil(totalBytes / normalizedPartSize));
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
partSize: normalizedPartSize,
|
|
27
|
+
partCount,
|
|
28
|
+
totalBytes,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function validateMultipartPartNumber(partNumber: number, partCount: number): void {
|
|
33
|
+
if (!Number.isInteger(partNumber) || partNumber < 1 || partNumber > partCount) {
|
|
34
|
+
throw Object.assign(
|
|
35
|
+
new Error(`Invalid multipart part number ${partNumber}. Expected 1..${partCount}.`),
|
|
36
|
+
{
|
|
37
|
+
code: 'MULTIPART_FAILED',
|
|
38
|
+
retryable: false,
|
|
39
|
+
taskId: '',
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": [
|
|
4
|
+
"dom",
|
|
5
|
+
"DOM.Iterable",
|
|
6
|
+
"esnext"
|
|
7
|
+
],
|
|
8
|
+
"types": [
|
|
9
|
+
"jest"
|
|
10
|
+
],
|
|
11
|
+
"typeRoots": [
|
|
12
|
+
"./ts-declarations",
|
|
13
|
+
"./node_modules/@types"
|
|
14
|
+
],
|
|
15
|
+
"jsx": "react-native",
|
|
16
|
+
"target": "esnext",
|
|
17
|
+
"moduleResolution": "bundler",
|
|
18
|
+
"module": "esnext",
|
|
19
|
+
"moduleDetection": "force",
|
|
20
|
+
"esModuleInterop": true,
|
|
21
|
+
"sourceMap": true,
|
|
22
|
+
"declaration": true,
|
|
23
|
+
"declarationMap": true,
|
|
24
|
+
"inlineSources": true,
|
|
25
|
+
"skipLibCheck": true,
|
|
26
|
+
"strict": true,
|
|
27
|
+
"noFallthroughCasesInSwitch": true,
|
|
28
|
+
"noPropertyAccessFromIndexSignature": false,
|
|
29
|
+
"noImplicitReturns": true,
|
|
30
|
+
"noUnusedLocals": true,
|
|
31
|
+
"noUnusedParameters": false,
|
|
32
|
+
"rootDir": "./src",
|
|
33
|
+
"outDir": "./build"
|
|
34
|
+
},
|
|
35
|
+
"include": [
|
|
36
|
+
"./src"
|
|
37
|
+
],
|
|
38
|
+
"exclude": [
|
|
39
|
+
"**/__mocks__/*",
|
|
40
|
+
"**/__tests__/*"
|
|
41
|
+
]
|
|
42
|
+
}
|