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,26 @@
|
|
|
1
|
+
import type { CompleteMultipartInput, CompleteMultipartResult, CreateMultipartInput, CreateMultipartResult, SignPartInput, SignedPartResult, UploadHttpMethod, UploadProvider } from '../Uploados.types';
|
|
2
|
+
export type ProviderUploadUrlInput = {
|
|
3
|
+
key: string;
|
|
4
|
+
contentType?: string;
|
|
5
|
+
metadata?: Record<string, string>;
|
|
6
|
+
};
|
|
7
|
+
export type ProviderUploadUrlResult = {
|
|
8
|
+
url: string;
|
|
9
|
+
method?: UploadHttpMethod;
|
|
10
|
+
headers?: Record<string, string>;
|
|
11
|
+
};
|
|
12
|
+
export type UploadProviderDefinition = {
|
|
13
|
+
getUploadUrl?: (input: ProviderUploadUrlInput) => Promise<ProviderUploadUrlResult>;
|
|
14
|
+
createMultipartUpload?: (input: CreateMultipartInput) => Promise<CreateMultipartResult>;
|
|
15
|
+
signPart?: (input: SignPartInput) => Promise<SignedPartResult>;
|
|
16
|
+
completeMultipartUpload?: (input: CompleteMultipartInput) => Promise<CompleteMultipartResult>;
|
|
17
|
+
abortMultipartUpload?: (input: {
|
|
18
|
+
key: string;
|
|
19
|
+
uploadId: string;
|
|
20
|
+
}) => Promise<void>;
|
|
21
|
+
};
|
|
22
|
+
export declare function defineUploadProvider(definition: UploadProviderDefinition): UploadProvider;
|
|
23
|
+
export declare function assertUploadProvider(provider: UploadProvider | undefined): UploadProvider;
|
|
24
|
+
export declare function assertProviderMultipartSupported(provider: UploadProvider): void;
|
|
25
|
+
export declare function assertProviderDirectUploadSupported(provider: UploadProvider): void;
|
|
26
|
+
//# sourceMappingURL=defineUploadProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defineUploadProvider.d.ts","sourceRoot":"","sources":["../../src/providers/defineUploadProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,uBAAuB,EACvB,oBAAoB,EACpB,qBAAqB,EACrB,aAAa,EACb,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACf,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,sBAAsB,GAAG;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,OAAO,CAAC,uBAAuB,CAAC,CAAC;IACnF,qBAAqB,CAAC,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACxF,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC/D,uBAAuB,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAC9F,oBAAoB,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACpF,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,wBAAwB,GAAG,cAAc,CAQzF;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,cAAc,GAAG,SAAS,GAAG,cAAc,CAKzF;AAED,wBAAgB,gCAAgC,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAU/E;AAED,wBAAgB,mCAAmC,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAIlF"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function defineUploadProvider(definition) {
|
|
2
|
+
return {
|
|
3
|
+
getUploadUrl: definition.getUploadUrl,
|
|
4
|
+
createMultipartUpload: definition.createMultipartUpload,
|
|
5
|
+
signPart: definition.signPart,
|
|
6
|
+
completeMultipartUpload: definition.completeMultipartUpload,
|
|
7
|
+
abortMultipartUpload: definition.abortMultipartUpload,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function assertUploadProvider(provider) {
|
|
11
|
+
if (!provider) {
|
|
12
|
+
throw providerError('Upload provider is required for provider-based uploads.');
|
|
13
|
+
}
|
|
14
|
+
return provider;
|
|
15
|
+
}
|
|
16
|
+
export function assertProviderMultipartSupported(provider) {
|
|
17
|
+
if (typeof provider.createMultipartUpload !== 'function') {
|
|
18
|
+
throw providerError('Provider must implement createMultipartUpload.');
|
|
19
|
+
}
|
|
20
|
+
if (typeof provider.signPart !== 'function') {
|
|
21
|
+
throw providerError('Provider must implement signPart.');
|
|
22
|
+
}
|
|
23
|
+
if (typeof provider.completeMultipartUpload !== 'function') {
|
|
24
|
+
throw providerError('Provider must implement completeMultipartUpload.');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function assertProviderDirectUploadSupported(provider) {
|
|
28
|
+
if (typeof provider.getUploadUrl !== 'function') {
|
|
29
|
+
throw providerError('Provider must implement getUploadUrl for direct signed-URL uploads.');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function providerError(message) {
|
|
33
|
+
return Object.assign(new Error(message), {
|
|
34
|
+
code: 'PROVIDER_ERROR',
|
|
35
|
+
retryable: false,
|
|
36
|
+
taskId: '',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=defineUploadProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defineUploadProvider.js","sourceRoot":"","sources":["../../src/providers/defineUploadProvider.ts"],"names":[],"mappings":"AA+BA,MAAM,UAAU,oBAAoB,CAAC,UAAoC;IACvE,OAAO;QACL,YAAY,EAAE,UAAU,CAAC,YAAY;QACrC,qBAAqB,EAAE,UAAU,CAAC,qBAAqB;QACvD,QAAQ,EAAE,UAAU,CAAC,QAAQ;QAC7B,uBAAuB,EAAE,UAAU,CAAC,uBAAuB;QAC3D,oBAAoB,EAAE,UAAU,CAAC,oBAAoB;KACtD,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,QAAoC;IACvE,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,aAAa,CAAC,yDAAyD,CAAC,CAAC;IACjF,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,gCAAgC,CAAC,QAAwB;IACvE,IAAI,OAAO,QAAQ,CAAC,qBAAqB,KAAK,UAAU,EAAE,CAAC;QACzD,MAAM,aAAa,CAAC,gDAAgD,CAAC,CAAC;IACxE,CAAC;IACD,IAAI,OAAO,QAAQ,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC;QAC5C,MAAM,aAAa,CAAC,mCAAmC,CAAC,CAAC;IAC3D,CAAC;IACD,IAAI,OAAO,QAAQ,CAAC,uBAAuB,KAAK,UAAU,EAAE,CAAC;QAC3D,MAAM,aAAa,CAAC,kDAAkD,CAAC,CAAC;IAC1E,CAAC;AACH,CAAC;AAED,MAAM,UAAU,mCAAmC,CAAC,QAAwB;IAC1E,IAAI,OAAO,QAAQ,CAAC,YAAY,KAAK,UAAU,EAAE,CAAC;QAChD,MAAM,aAAa,CAAC,qEAAqE,CAAC,CAAC;IAC7F,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CACpB,OAAe;IAEf,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,EAAE;QACvC,IAAI,EAAE,gBAAgB;QACtB,SAAS,EAAE,KAAK;QAChB,MAAM,EAAE,EAAE;KACX,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type {\n CompleteMultipartInput,\n CompleteMultipartResult,\n CreateMultipartInput,\n CreateMultipartResult,\n SignPartInput,\n SignedPartResult,\n UploadHttpMethod,\n UploadProvider,\n} from '../Uploados.types';\n\nexport type ProviderUploadUrlInput = {\n key: string;\n contentType?: string;\n metadata?: Record<string, string>;\n};\n\nexport type ProviderUploadUrlResult = {\n url: string;\n method?: UploadHttpMethod;\n headers?: Record<string, string>;\n};\n\nexport type UploadProviderDefinition = {\n getUploadUrl?: (input: ProviderUploadUrlInput) => Promise<ProviderUploadUrlResult>;\n createMultipartUpload?: (input: CreateMultipartInput) => Promise<CreateMultipartResult>;\n signPart?: (input: SignPartInput) => Promise<SignedPartResult>;\n completeMultipartUpload?: (input: CompleteMultipartInput) => Promise<CompleteMultipartResult>;\n abortMultipartUpload?: (input: { key: string; uploadId: string }) => Promise<void>;\n};\n\nexport function defineUploadProvider(definition: UploadProviderDefinition): UploadProvider {\n return {\n getUploadUrl: definition.getUploadUrl,\n createMultipartUpload: definition.createMultipartUpload,\n signPart: definition.signPart,\n completeMultipartUpload: definition.completeMultipartUpload,\n abortMultipartUpload: definition.abortMultipartUpload,\n };\n}\n\nexport function assertUploadProvider(provider: UploadProvider | undefined): UploadProvider {\n if (!provider) {\n throw providerError('Upload provider is required for provider-based uploads.');\n }\n return provider;\n}\n\nexport function assertProviderMultipartSupported(provider: UploadProvider): void {\n if (typeof provider.createMultipartUpload !== 'function') {\n throw providerError('Provider must implement createMultipartUpload.');\n }\n if (typeof provider.signPart !== 'function') {\n throw providerError('Provider must implement signPart.');\n }\n if (typeof provider.completeMultipartUpload !== 'function') {\n throw providerError('Provider must implement completeMultipartUpload.');\n }\n}\n\nexport function assertProviderDirectUploadSupported(provider: UploadProvider): void {\n if (typeof provider.getUploadUrl !== 'function') {\n throw providerError('Provider must implement getUploadUrl for direct signed-URL uploads.');\n }\n}\n\nfunction providerError(\n message: string\n): Error & { code: string; retryable: boolean; taskId: string } {\n return Object.assign(new Error(message), {\n code: 'PROVIDER_ERROR',\n retryable: false,\n taskId: '',\n });\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const DEFAULT_MULTIPART_PART_SIZE: number;
|
|
2
|
+
export declare const MIN_MULTIPART_PART_SIZE: number;
|
|
3
|
+
export type MultipartUploadPlan = {
|
|
4
|
+
partSize: number;
|
|
5
|
+
partCount: number;
|
|
6
|
+
totalBytes: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function createMultipartUploadPlan(totalBytes: number, partSize?: number): MultipartUploadPlan;
|
|
9
|
+
export declare function validateMultipartPartNumber(partNumber: number, partCount: number): void;
|
|
10
|
+
//# sourceMappingURL=multipartPlan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multipartPlan.d.ts","sourceRoot":"","sources":["../../src/providers/multipartPlan.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,2BAA2B,QAAkB,CAAC;AAC3D,eAAO,MAAM,uBAAuB,QAAkB,CAAC;AAEvD,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,MAAM,EAClB,QAAQ,GAAE,MAAoC,GAC7C,mBAAmB,CAiBrB;AAED,wBAAgB,2BAA2B,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAWvF"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const DEFAULT_MULTIPART_PART_SIZE = 8 * 1024 * 1024;
|
|
2
|
+
export const MIN_MULTIPART_PART_SIZE = 5 * 1024 * 1024;
|
|
3
|
+
export function createMultipartUploadPlan(totalBytes, partSize = DEFAULT_MULTIPART_PART_SIZE) {
|
|
4
|
+
if (!Number.isFinite(totalBytes) || totalBytes <= 0) {
|
|
5
|
+
throw Object.assign(new Error('File size must be greater than zero for multipart uploads.'), {
|
|
6
|
+
code: 'FILE_NOT_FOUND',
|
|
7
|
+
retryable: false,
|
|
8
|
+
taskId: '',
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
const normalizedPartSize = Math.max(MIN_MULTIPART_PART_SIZE, Math.floor(partSize));
|
|
12
|
+
const partCount = Math.max(1, Math.ceil(totalBytes / normalizedPartSize));
|
|
13
|
+
return {
|
|
14
|
+
partSize: normalizedPartSize,
|
|
15
|
+
partCount,
|
|
16
|
+
totalBytes,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function validateMultipartPartNumber(partNumber, partCount) {
|
|
20
|
+
if (!Number.isInteger(partNumber) || partNumber < 1 || partNumber > partCount) {
|
|
21
|
+
throw Object.assign(new Error(`Invalid multipart part number ${partNumber}. Expected 1..${partCount}.`), {
|
|
22
|
+
code: 'MULTIPART_FAILED',
|
|
23
|
+
retryable: false,
|
|
24
|
+
taskId: '',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=multipartPlan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multipartPlan.js","sourceRoot":"","sources":["../../src/providers/multipartPlan.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAC3D,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAQvD,MAAM,UAAU,yBAAyB,CACvC,UAAkB,EAClB,WAAmB,2BAA2B;IAE9C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,UAAU,IAAI,CAAC,EAAE,CAAC;QACpD,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,4DAA4D,CAAC,EAAE;YAC3F,IAAI,EAAE,gBAAgB;YACtB,SAAS,EAAE,KAAK;YAChB,MAAM,EAAE,EAAE;SACX,CAAC,CAAC;IACL,CAAC;IAED,MAAM,kBAAkB,GAAG,IAAI,CAAC,GAAG,CAAC,uBAAuB,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC;IACnF,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,GAAG,kBAAkB,CAAC,CAAC,CAAC;IAE1E,OAAO;QACL,QAAQ,EAAE,kBAAkB;QAC5B,SAAS;QACT,UAAU;KACX,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,UAAkB,EAAE,SAAiB;IAC/E,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,UAAU,GAAG,CAAC,IAAI,UAAU,GAAG,SAAS,EAAE,CAAC;QAC9E,MAAM,MAAM,CAAC,MAAM,CACjB,IAAI,KAAK,CAAC,iCAAiC,UAAU,iBAAiB,SAAS,GAAG,CAAC,EACnF;YACE,IAAI,EAAE,kBAAkB;YACxB,SAAS,EAAE,KAAK;YAChB,MAAM,EAAE,EAAE;SACX,CACF,CAAC;IACJ,CAAC;AACH,CAAC","sourcesContent":["export const DEFAULT_MULTIPART_PART_SIZE = 8 * 1024 * 1024;\nexport const MIN_MULTIPART_PART_SIZE = 5 * 1024 * 1024;\n\nexport type MultipartUploadPlan = {\n partSize: number;\n partCount: number;\n totalBytes: number;\n};\n\nexport function createMultipartUploadPlan(\n totalBytes: number,\n partSize: number = DEFAULT_MULTIPART_PART_SIZE\n): MultipartUploadPlan {\n if (!Number.isFinite(totalBytes) || totalBytes <= 0) {\n throw Object.assign(new Error('File size must be greater than zero for multipart uploads.'), {\n code: 'FILE_NOT_FOUND',\n retryable: false,\n taskId: '',\n });\n }\n\n const normalizedPartSize = Math.max(MIN_MULTIPART_PART_SIZE, Math.floor(partSize));\n const partCount = Math.max(1, Math.ceil(totalBytes / normalizedPartSize));\n\n return {\n partSize: normalizedPartSize,\n partCount,\n totalBytes,\n };\n}\n\nexport function validateMultipartPartNumber(partNumber: number, partCount: number): void {\n if (!Number.isInteger(partNumber) || partNumber < 1 || partNumber > partCount) {\n throw Object.assign(\n new Error(`Invalid multipart part number ${partNumber}. Expected 1..${partCount}.`),\n {\n code: 'MULTIPART_FAILED',\n retryable: false,\n taskId: '',\n }\n );\n }\n}\n"]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
const { defineConfig } = require('eslint/config');
|
|
2
|
+
const universe = require('eslint-config-universe/flat/native');
|
|
3
|
+
const universeWeb = require('eslint-config-universe/flat/web');
|
|
4
|
+
|
|
5
|
+
module.exports = defineConfig([{ ignores: ['build'] }, ...universe, ...universeWeb]);
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import ImageIO
|
|
3
|
+
import UIKit
|
|
4
|
+
import UniformTypeIdentifiers
|
|
5
|
+
|
|
6
|
+
enum CompressionPreset: String, Codable {
|
|
7
|
+
case balanced
|
|
8
|
+
case inspection
|
|
9
|
+
case avatar
|
|
10
|
+
|
|
11
|
+
init(raw: String) {
|
|
12
|
+
self = CompressionPreset(rawValue: raw) ?? .balanced
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
var maxDimension: CGFloat {
|
|
16
|
+
switch self {
|
|
17
|
+
case .balanced:
|
|
18
|
+
return 2048
|
|
19
|
+
case .inspection:
|
|
20
|
+
return 4096
|
|
21
|
+
case .avatar:
|
|
22
|
+
return 512
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
var jpegQuality: CGFloat {
|
|
27
|
+
switch self {
|
|
28
|
+
case .balanced:
|
|
29
|
+
return 0.82
|
|
30
|
+
case .inspection:
|
|
31
|
+
return 0.92
|
|
32
|
+
case .avatar:
|
|
33
|
+
return 0.85
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
struct CompressionStats: Codable {
|
|
39
|
+
let preset: CompressionPreset
|
|
40
|
+
let originalSize: Int
|
|
41
|
+
let optimizedSize: Int
|
|
42
|
+
let format: String
|
|
43
|
+
|
|
44
|
+
func toDictionary() -> [String: Any] {
|
|
45
|
+
[
|
|
46
|
+
"preset": preset.rawValue,
|
|
47
|
+
"originalSize": originalSize,
|
|
48
|
+
"optimizedSize": optimizedSize,
|
|
49
|
+
"format": format,
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
struct CompressionResult {
|
|
55
|
+
let outputURL: URL
|
|
56
|
+
let stats: CompressionStats
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
enum CompressionPipeline {
|
|
60
|
+
private static let optimizedDirectoryName = "optimized"
|
|
61
|
+
|
|
62
|
+
static func optimizedDirectoryURL() -> URL? {
|
|
63
|
+
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?
|
|
64
|
+
.appendingPathComponent("uploados", isDirectory: true)
|
|
65
|
+
.appendingPathComponent(optimizedDirectoryName, isDirectory: true)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static func isSdkOwnedOptimizedPath(_ path: String) -> Bool {
|
|
69
|
+
guard let root = optimizedDirectoryURL()?.path else {
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
return path.hasPrefix(root + "/") || path == root
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static func cleanupOptimizedFile(path: String) {
|
|
76
|
+
guard isSdkOwnedOptimizedPath(path) else {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
try? FileManager.default.removeItem(atPath: path)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
static func compress(sourceURL: URL, taskId: String, preset: CompressionPreset) throws -> CompressionResult {
|
|
83
|
+
guard let directory = optimizedDirectoryURL() else {
|
|
84
|
+
throw CompressionPipelineError.directoryUnavailable
|
|
85
|
+
}
|
|
86
|
+
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
87
|
+
let destination = directory.appendingPathComponent("\(taskId).jpg")
|
|
88
|
+
if FileManager.default.fileExists(atPath: destination.path) {
|
|
89
|
+
let attributes = try FileManager.default.attributesOfItem(atPath: sourceURL.path)
|
|
90
|
+
let originalSize = (attributes[.size] as? NSNumber)?.intValue ?? 0
|
|
91
|
+
let optimizedAttributes = try FileManager.default.attributesOfItem(atPath: destination.path)
|
|
92
|
+
let optimizedSize = (optimizedAttributes[.size] as? NSNumber)?.intValue ?? 0
|
|
93
|
+
return CompressionResult(
|
|
94
|
+
outputURL: destination,
|
|
95
|
+
stats: CompressionStats(
|
|
96
|
+
preset: preset,
|
|
97
|
+
originalSize: originalSize,
|
|
98
|
+
optimizedSize: optimizedSize,
|
|
99
|
+
format: "jpeg"
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
guard let source = CGImageSourceCreateWithURL(sourceURL as CFURL, nil) else {
|
|
105
|
+
throw CompressionPipelineError.unsupportedInput
|
|
106
|
+
}
|
|
107
|
+
guard let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any],
|
|
108
|
+
let width = properties[kCGImagePropertyPixelWidth] as? NSNumber,
|
|
109
|
+
let height = properties[kCGImagePropertyPixelHeight] as? NSNumber else {
|
|
110
|
+
throw CompressionPipelineError.unsupportedInput
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let originalAttributes = try FileManager.default.attributesOfItem(atPath: sourceURL.path)
|
|
114
|
+
let originalSize = (originalAttributes[.size] as? NSNumber)?.intValue ?? 0
|
|
115
|
+
let maxDimension = preset.maxDimension
|
|
116
|
+
let scale = min(
|
|
117
|
+
1,
|
|
118
|
+
maxDimension / max(CGFloat(truncating: width), CGFloat(truncating: height))
|
|
119
|
+
)
|
|
120
|
+
let targetWidth = Int(CGFloat(truncating: width) * scale)
|
|
121
|
+
let targetHeight = Int(CGFloat(truncating: height) * scale)
|
|
122
|
+
|
|
123
|
+
let options: [CFString: Any] = [
|
|
124
|
+
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
|
125
|
+
kCGImageSourceCreateThumbnailWithTransform: true,
|
|
126
|
+
kCGImageSourceThumbnailMaxPixelSize: max(targetWidth, targetHeight),
|
|
127
|
+
]
|
|
128
|
+
guard let image = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
|
|
129
|
+
throw CompressionPipelineError.compressionFailed
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
guard let destinationRef = CGImageDestinationCreateWithURL(
|
|
133
|
+
destination as CFURL,
|
|
134
|
+
UTType.jpeg.identifier as CFString,
|
|
135
|
+
1,
|
|
136
|
+
nil
|
|
137
|
+
) else {
|
|
138
|
+
throw CompressionPipelineError.compressionFailed
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let jpegProperties: [CFString: Any] = [
|
|
142
|
+
kCGImageDestinationLossyCompressionQuality: preset.jpegQuality,
|
|
143
|
+
kCGImagePropertyOrientation: properties[kCGImagePropertyOrientation] ?? 1,
|
|
144
|
+
]
|
|
145
|
+
CGImageDestinationAddImage(destinationRef, image, jpegProperties as CFDictionary)
|
|
146
|
+
guard CGImageDestinationFinalize(destinationRef) else {
|
|
147
|
+
throw CompressionPipelineError.compressionFailed
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let optimizedAttributes = try FileManager.default.attributesOfItem(atPath: destination.path)
|
|
151
|
+
let optimizedSize = (optimizedAttributes[.size] as? NSNumber)?.intValue ?? 0
|
|
152
|
+
guard optimizedSize > 0 else {
|
|
153
|
+
throw CompressionPipelineError.compressionFailed
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return CompressionResult(
|
|
157
|
+
outputURL: destination,
|
|
158
|
+
stats: CompressionStats(
|
|
159
|
+
preset: preset,
|
|
160
|
+
originalSize: originalSize,
|
|
161
|
+
optimizedSize: optimizedSize,
|
|
162
|
+
format: "jpeg"
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
enum CompressionPipelineError: Error, LocalizedError {
|
|
169
|
+
case directoryUnavailable
|
|
170
|
+
case unsupportedInput
|
|
171
|
+
case compressionFailed
|
|
172
|
+
|
|
173
|
+
var errorDescription: String? {
|
|
174
|
+
switch self {
|
|
175
|
+
case .directoryUnavailable:
|
|
176
|
+
return "Compression output directory is unavailable."
|
|
177
|
+
case .unsupportedInput:
|
|
178
|
+
return "The selected file is not a supported image format."
|
|
179
|
+
case .compressionFailed:
|
|
180
|
+
return "Image compression failed."
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum FileStager {
|
|
4
|
+
private static let stagedDirectoryName = "staged"
|
|
5
|
+
|
|
6
|
+
static func stagedDirectoryURL() -> URL? {
|
|
7
|
+
FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?
|
|
8
|
+
.appendingPathComponent("uploados", isDirectory: true)
|
|
9
|
+
.appendingPathComponent(stagedDirectoryName, isDirectory: true)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
static func needsStaging(uri: String, fileURL: URL) -> Bool {
|
|
13
|
+
if uri.hasPrefix("content://") {
|
|
14
|
+
return true
|
|
15
|
+
}
|
|
16
|
+
let path = fileURL.path
|
|
17
|
+
if path.contains("-Inbox/") {
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
20
|
+
if path.contains("/tmp/") {
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
if path.contains("/Library/Caches/") {
|
|
24
|
+
return true
|
|
25
|
+
}
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static func stageFile(sourceURL: URL, taskId: String) throws -> URL {
|
|
30
|
+
guard let directory = stagedDirectoryURL() else {
|
|
31
|
+
throw FileStagerError.directoryUnavailable
|
|
32
|
+
}
|
|
33
|
+
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
|
34
|
+
let ext = sourceURL.pathExtension.isEmpty ? "" : ".\(sourceURL.pathExtension)"
|
|
35
|
+
let destination = directory.appendingPathComponent("\(taskId)\(ext)")
|
|
36
|
+
if FileManager.default.fileExists(atPath: destination.path) {
|
|
37
|
+
return destination
|
|
38
|
+
}
|
|
39
|
+
try FileManager.default.copyItem(at: sourceURL, to: destination)
|
|
40
|
+
return destination
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static func isSdkOwnedStagedPath(_ path: String) -> Bool {
|
|
44
|
+
guard let stagedRoot = stagedDirectoryURL()?.path else {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
return path.hasPrefix(stagedRoot + "/") || path == stagedRoot
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static func cleanupStagedFile(path: String) {
|
|
51
|
+
guard isSdkOwnedStagedPath(path) else {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
try? FileManager.default.removeItem(atPath: path)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
enum FileStagerError: Error, LocalizedError {
|
|
59
|
+
case directoryUnavailable
|
|
60
|
+
|
|
61
|
+
var errorDescription: String? {
|
|
62
|
+
switch self {
|
|
63
|
+
case .directoryUnavailable:
|
|
64
|
+
return "Upload staging directory is unavailable."
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|