@umituz/react-native-filesystem 2.1.16 → 2.1.17
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/package.json +6 -2
- package/src/domain/constants/FileConstants.ts +20 -0
- package/src/domain/entities/File.ts +15 -127
- package/src/domain/types/FileTypes.ts +43 -0
- package/src/domain/utils/FileUtils.ts +86 -0
- package/src/index.ts +4 -26
- package/src/infrastructure/services/FileSystemService.ts +29 -139
- package/src/infrastructure/services/cache.service.ts +2 -2
- package/src/infrastructure/services/directory.service.ts +3 -3
- package/src/infrastructure/services/download.service.ts +58 -150
- package/src/infrastructure/services/download.types.ts +7 -0
- package/src/infrastructure/services/encoding.service.ts +6 -5
- package/src/infrastructure/services/file-info.service.ts +2 -2
- package/src/infrastructure/services/file-manager.service.ts +1 -1
- package/src/infrastructure/services/file-reader.service.ts +3 -22
- package/src/infrastructure/services/file-writer.service.ts +4 -4
- package/src/infrastructure/utils/blob.utils.ts +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-filesystem",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.17",
|
|
4
4
|
"description": "File operations and import/export functionality for React Native apps",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -36,10 +36,14 @@
|
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/react": "~19.1.10",
|
|
39
|
+
"@typescript-eslint/eslint-plugin": "^8.52.0",
|
|
40
|
+
"@typescript-eslint/parser": "^8.52.0",
|
|
41
|
+
"eslint": "^9.39.2",
|
|
39
42
|
"expo-file-system": "^19.0.21",
|
|
40
43
|
"react": "19.1.0",
|
|
41
44
|
"react-native": "0.81.5",
|
|
42
|
-
"typescript": "~5.3.0"
|
|
45
|
+
"typescript": "~5.3.0",
|
|
46
|
+
"typescript-eslint": "^8.52.0"
|
|
43
47
|
},
|
|
44
48
|
"publishConfig": {
|
|
45
49
|
"access": "public"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Constants
|
|
3
|
+
* File-related constants for the filesystem package
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { FileEncoding } from '../types/FileTypes';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* File-related constants
|
|
10
|
+
*/
|
|
11
|
+
export const FILE_CONSTANTS = {
|
|
12
|
+
MAX_FILE_SIZE: 100 * 1024 * 1024, // 100 MB
|
|
13
|
+
ALLOWED_EXTENSIONS: ['.jpg', '.jpeg', '.png', '.pdf', '.txt', '.json', '.mp4', '.mp3'] as const,
|
|
14
|
+
DEFAULT_ENCODING: 'utf8' as FileEncoding,
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Allowed file extensions type
|
|
19
|
+
*/
|
|
20
|
+
export type AllowedExtension = typeof FILE_CONSTANTS.ALLOWED_EXTENSIONS[number];
|
|
@@ -1,132 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* File Entity
|
|
3
|
-
* Domain layer
|
|
2
|
+
* File Entity - Module Index
|
|
3
|
+
* Domain layer exports for file operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export type DirectoryType = 'documentDirectory' | 'cacheDirectory';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Result of a file operation
|
|
18
|
-
*/
|
|
19
|
-
export interface FileOperationResult {
|
|
20
|
-
success: boolean;
|
|
21
|
-
uri?: string;
|
|
22
|
-
error?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* File information metadata
|
|
27
|
-
*/
|
|
28
|
-
export interface FileInfo {
|
|
29
|
-
uri: string;
|
|
30
|
-
name?: string;
|
|
31
|
-
size: number;
|
|
32
|
-
exists: boolean;
|
|
33
|
-
isDirectory: boolean;
|
|
34
|
-
modificationTime?: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Download progress information
|
|
39
|
-
*/
|
|
40
|
-
export interface DownloadProgress {
|
|
41
|
-
totalBytesWritten: number;
|
|
42
|
-
totalBytesExpectedToWrite: number;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* File-related constants
|
|
47
|
-
*/
|
|
48
|
-
export const FILE_CONSTANTS = {
|
|
49
|
-
MAX_FILE_SIZE: 100 * 1024 * 1024, // 100 MB
|
|
50
|
-
ALLOWED_EXTENSIONS: ['.jpg', '.jpeg', '.png', '.pdf', '.txt', '.json', '.mp4', '.mp3'],
|
|
51
|
-
DEFAULT_ENCODING: 'utf8' as FileEncoding,
|
|
52
|
-
} as const;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* File utility functions
|
|
56
|
-
*/
|
|
57
|
-
export class FileUtils {
|
|
58
|
-
/**
|
|
59
|
-
* Format file size in bytes to human-readable format
|
|
60
|
-
*/
|
|
61
|
-
static formatFileSize(bytes: number): string {
|
|
62
|
-
if (bytes === 0) return '0 Bytes';
|
|
63
|
-
const k = 1024;
|
|
64
|
-
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
65
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
66
|
-
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Generate unique filename with timestamp
|
|
71
|
-
*/
|
|
72
|
-
static generateUniqueFilename(filename: string): string {
|
|
73
|
-
const timestamp = Date.now();
|
|
74
|
-
const extension = filename.includes('.') ? filename.substring(filename.lastIndexOf('.')) : '';
|
|
75
|
-
const nameWithoutExt = filename.includes('.')
|
|
76
|
-
? filename.substring(0, filename.lastIndexOf('.'))
|
|
77
|
-
: filename;
|
|
78
|
-
return `${nameWithoutExt}_${timestamp}${extension}`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Sanitize filename by removing invalid characters
|
|
83
|
-
*/
|
|
84
|
-
static sanitizeFilename(filename: string): string {
|
|
85
|
-
return filename.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Join path segments
|
|
90
|
-
*/
|
|
91
|
-
static joinPaths(...segments: string[]): string {
|
|
92
|
-
return segments
|
|
93
|
-
.map((segment, index) => {
|
|
94
|
-
if (index === 0) {
|
|
95
|
-
return segment.replace(/\/+$/, '');
|
|
96
|
-
}
|
|
97
|
-
return segment.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
98
|
-
})
|
|
99
|
-
.filter(segment => segment.length > 0)
|
|
100
|
-
.join('/');
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Get file extension from filename
|
|
105
|
-
*/
|
|
106
|
-
static getFileExtension(filename: string): string {
|
|
107
|
-
const lastDot = filename.lastIndexOf('.');
|
|
108
|
-
return lastDot > 0 ? filename.substring(lastDot) : '';
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Check if file extension is allowed
|
|
113
|
-
*/
|
|
114
|
-
static isAllowedExtension(filename: string): boolean {
|
|
115
|
-
const extension = this.getFileExtension(filename).toLowerCase();
|
|
116
|
-
return FILE_CONSTANTS.ALLOWED_EXTENSIONS.includes(extension as typeof FILE_CONSTANTS.ALLOWED_EXTENSIONS[number]);
|
|
117
|
-
}
|
|
6
|
+
// Types
|
|
7
|
+
export type {
|
|
8
|
+
FileEncoding,
|
|
9
|
+
DirectoryType,
|
|
10
|
+
FileOperationResult,
|
|
11
|
+
FileInfo,
|
|
12
|
+
DownloadProgress,
|
|
13
|
+
} from '../types/FileTypes';
|
|
118
14
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
static getFilenameFromUri(uri: string): string {
|
|
123
|
-
return uri.split('/').pop() || '';
|
|
124
|
-
}
|
|
15
|
+
// Constants
|
|
16
|
+
export { FILE_CONSTANTS } from '../constants/FileConstants';
|
|
17
|
+
export type { AllowedExtension } from '../constants/FileConstants';
|
|
125
18
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
*/
|
|
129
|
-
static isValidFileSize(size: number): boolean {
|
|
130
|
-
return size > 0 && size <= FILE_CONSTANTS.MAX_FILE_SIZE;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
19
|
+
// Utils
|
|
20
|
+
export { FileUtils } from '../utils/FileUtils';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Types
|
|
3
|
+
* Domain layer type definitions for file operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* File encoding types supported by expo-file-system
|
|
8
|
+
*/
|
|
9
|
+
export type FileEncoding = 'utf8' | 'base64';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Directory types available in expo-file-system
|
|
13
|
+
*/
|
|
14
|
+
export type DirectoryType = 'documentDirectory' | 'cacheDirectory';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Result of a file operation
|
|
18
|
+
*/
|
|
19
|
+
export interface FileOperationResult {
|
|
20
|
+
success: boolean;
|
|
21
|
+
uri?: string;
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* File information metadata
|
|
27
|
+
*/
|
|
28
|
+
export interface FileInfo {
|
|
29
|
+
uri: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
size: number;
|
|
32
|
+
exists: boolean;
|
|
33
|
+
isDirectory: boolean;
|
|
34
|
+
modificationTime?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Download progress information
|
|
39
|
+
*/
|
|
40
|
+
export interface DownloadProgress {
|
|
41
|
+
totalBytesWritten: number;
|
|
42
|
+
totalBytesExpectedToWrite: number;
|
|
43
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Utils
|
|
3
|
+
* File utility functions for common operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { FILE_CONSTANTS, type AllowedExtension } from '../constants/FileConstants';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* File utility functions
|
|
10
|
+
*/
|
|
11
|
+
export class FileUtils {
|
|
12
|
+
/**
|
|
13
|
+
* Format file size in bytes to human-readable format
|
|
14
|
+
*/
|
|
15
|
+
static formatFileSize(bytes: number): string {
|
|
16
|
+
if (bytes === 0) return '0 Bytes';
|
|
17
|
+
const k = 1024;
|
|
18
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
19
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
20
|
+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate unique filename with timestamp
|
|
25
|
+
*/
|
|
26
|
+
static generateUniqueFilename(filename: string): string {
|
|
27
|
+
const timestamp = Date.now();
|
|
28
|
+
const extension = filename.includes('.') ? filename.substring(filename.lastIndexOf('.')) : '';
|
|
29
|
+
const nameWithoutExt = filename.includes('.')
|
|
30
|
+
? filename.substring(0, filename.lastIndexOf('.'))
|
|
31
|
+
: filename;
|
|
32
|
+
return `${nameWithoutExt}_${timestamp}${extension}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Sanitize filename by removing invalid characters
|
|
37
|
+
*/
|
|
38
|
+
static sanitizeFilename(filename: string): string {
|
|
39
|
+
return filename.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Join path segments
|
|
44
|
+
*/
|
|
45
|
+
static joinPaths(...segments: string[]): string {
|
|
46
|
+
return segments
|
|
47
|
+
.map((segment, index) => {
|
|
48
|
+
if (index === 0) {
|
|
49
|
+
return segment.replace(/\/+$/, '');
|
|
50
|
+
}
|
|
51
|
+
return segment.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
52
|
+
})
|
|
53
|
+
.filter(segment => segment.length > 0)
|
|
54
|
+
.join('/');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get file extension from filename
|
|
59
|
+
*/
|
|
60
|
+
static getFileExtension(filename: string): string {
|
|
61
|
+
const lastDot = filename.lastIndexOf('.');
|
|
62
|
+
return lastDot > 0 ? filename.substring(lastDot) : '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if file extension is allowed
|
|
67
|
+
*/
|
|
68
|
+
static isAllowedExtension(filename: string): boolean {
|
|
69
|
+
const extension = this.getFileExtension(filename).toLowerCase();
|
|
70
|
+
return FILE_CONSTANTS.ALLOWED_EXTENSIONS.includes(extension as AllowedExtension);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Get filename from URI
|
|
75
|
+
*/
|
|
76
|
+
static getFilenameFromUri(uri: string): string {
|
|
77
|
+
return uri.split('/').pop() || '';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Validate file size
|
|
82
|
+
*/
|
|
83
|
+
static isValidFileSize(size: number): boolean {
|
|
84
|
+
return size > 0 && size <= FILE_CONSTANTS.MAX_FILE_SIZE;
|
|
85
|
+
}
|
|
86
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,39 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @umituz/react-native-filesystem
|
|
3
|
-
* File operations for React Native apps
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
5
|
// Domain - Types and Utilities
|
|
7
|
-
export type {
|
|
8
|
-
FileEncoding,
|
|
9
|
-
DirectoryType,
|
|
10
|
-
FileOperationResult,
|
|
11
|
-
FileInfo,
|
|
12
|
-
DownloadProgress,
|
|
13
|
-
} from "./domain/entities/File";
|
|
14
|
-
|
|
6
|
+
export type { FileEncoding, DirectoryType, FileOperationResult, FileInfo, DownloadProgress } from "./domain/entities/File";
|
|
15
7
|
export { FILE_CONSTANTS, FileUtils } from "./domain/entities/File";
|
|
16
8
|
|
|
17
9
|
// Services - Download Operations
|
|
18
|
-
export type {
|
|
19
|
-
|
|
20
|
-
DownloadWithProgressResult,
|
|
21
|
-
} from "./infrastructure/services/download.service";
|
|
22
|
-
|
|
23
|
-
export {
|
|
24
|
-
downloadFile,
|
|
25
|
-
downloadFileWithProgress,
|
|
26
|
-
isUrlCached,
|
|
27
|
-
getCachedFileUri,
|
|
28
|
-
deleteCachedFile,
|
|
29
|
-
} from "./infrastructure/services/download.service";
|
|
10
|
+
export type { DownloadProgressCallback, DownloadWithProgressResult } from "./infrastructure/services/download.types";
|
|
11
|
+
export { downloadFile, downloadFileWithProgress, isUrlCached, getCachedFileUri, deleteCachedFile } from "./infrastructure/services/download.service";
|
|
30
12
|
|
|
31
13
|
// Services - Directory Operations
|
|
32
|
-
export {
|
|
33
|
-
getCacheDirectory,
|
|
34
|
-
getDocumentDirectory,
|
|
35
|
-
createDirectory,
|
|
36
|
-
} from "./infrastructure/services/directory.service";
|
|
14
|
+
export { getCacheDirectory, getDocumentDirectory, createDirectory } from "./infrastructure/services/directory.service";
|
|
37
15
|
|
|
38
16
|
// Services - File Operations
|
|
39
17
|
export { FileSystemService } from "./infrastructure/services/FileSystemService";
|
|
@@ -1,155 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FileSystem Service - Facade
|
|
3
|
-
* Delegates to specialized services following SOLID principles
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
5
|
import { readFile, readFileAsBase64 } from "./file-reader.service";
|
|
7
6
|
import { writeFile } from "./file-writer.service";
|
|
8
7
|
import { deleteFile, copyFile, moveFile } from "./file-manager.service";
|
|
9
|
-
import {
|
|
10
|
-
createDirectory,
|
|
11
|
-
listDirectory,
|
|
12
|
-
getDirectoryPath,
|
|
13
|
-
getDocumentDirectory,
|
|
14
|
-
getCacheDirectory,
|
|
15
|
-
} from "./directory.service";
|
|
8
|
+
import { createDirectory, listDirectory, getDirectoryPath, getDocumentDirectory, getCacheDirectory } from "./directory.service";
|
|
16
9
|
import { getFileInfo, fileExists, getFileSize } from "./file-info.service";
|
|
17
10
|
import { downloadFile } from "./download.service";
|
|
18
11
|
import { clearCache, getDirectorySize } from "./cache.service";
|
|
19
12
|
import { generateFilePath } from "./file-path.service";
|
|
20
13
|
import { FileUtils } from "../../domain/entities/File";
|
|
21
|
-
import type {
|
|
22
|
-
FileEncoding,
|
|
23
|
-
DirectoryType,
|
|
24
|
-
FileOperationResult,
|
|
25
|
-
} from "../../domain/entities/File";
|
|
14
|
+
import type { FileOperationResult } from "../../domain/entities/File";
|
|
26
15
|
|
|
27
|
-
/**
|
|
28
|
-
* FileSystem Service - Clean facade for all file operations
|
|
29
|
-
* Delegates to specialized services following Single Responsibility Principle
|
|
30
|
-
*/
|
|
31
16
|
export class FileSystemService {
|
|
32
|
-
|
|
33
|
-
static
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
static
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
static
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
sourceUri: string,
|
|
60
|
-
destinationUri: string
|
|
61
|
-
): Promise<FileOperationResult> {
|
|
62
|
-
return copyFile(sourceUri, destinationUri);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
static async moveFile(
|
|
66
|
-
sourceUri: string,
|
|
67
|
-
destinationUri: string
|
|
68
|
-
): Promise<FileOperationResult> {
|
|
69
|
-
return moveFile(sourceUri, destinationUri);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Directory Operations
|
|
73
|
-
static async createDirectory(uri: string): Promise<boolean> {
|
|
74
|
-
return createDirectory(uri);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
static async listDirectory(uri: string): Promise<string[]> {
|
|
78
|
-
return listDirectory(uri);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
static getDirectoryPath(type: DirectoryType): string {
|
|
82
|
-
return getDirectoryPath(type);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
static getDocumentDirectory(): string {
|
|
86
|
-
return getDocumentDirectory();
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
static getCacheDirectory(): string {
|
|
90
|
-
return getCacheDirectory();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// File Information
|
|
94
|
-
static async getFileInfo(uri: string) {
|
|
95
|
-
return getFileInfo(uri);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
static async exists(uri: string): Promise<boolean> {
|
|
99
|
-
return fileExists(uri);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
static async getFileSize(uri: string): Promise<number> {
|
|
103
|
-
return getFileSize(uri);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Downloads
|
|
107
|
-
static async downloadFile(
|
|
108
|
-
url: string,
|
|
109
|
-
destinationUri?: string
|
|
110
|
-
): Promise<FileOperationResult> {
|
|
111
|
-
return downloadFile(url, destinationUri);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Cache Management
|
|
115
|
-
static async clearCache(): Promise<boolean> {
|
|
116
|
-
return clearCache();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
static async getDirectorySize(uri: string): Promise<number> {
|
|
120
|
-
return getDirectorySize(uri);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// File Path Generation
|
|
124
|
-
static generateFilePath(
|
|
125
|
-
filename: string,
|
|
126
|
-
directory: DirectoryType = "documentDirectory"
|
|
127
|
-
): string {
|
|
128
|
-
return generateFilePath(filename, directory);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Convenience methods
|
|
132
|
-
static async copyToCache(
|
|
133
|
-
sourceUri: string,
|
|
134
|
-
filename?: string
|
|
135
|
-
): Promise<FileOperationResult> {
|
|
136
|
-
const cacheDir = getCacheDirectory();
|
|
137
|
-
const uniqueFilename = FileUtils.generateUniqueFilename(
|
|
138
|
-
filename || sourceUri.split("/").pop() || "file"
|
|
139
|
-
);
|
|
140
|
-
const destinationUri = FileUtils.joinPaths(cacheDir, uniqueFilename);
|
|
141
|
-
return copyFile(sourceUri, destinationUri);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
static async copyToDocuments(
|
|
145
|
-
sourceUri: string,
|
|
146
|
-
filename?: string
|
|
147
|
-
): Promise<FileOperationResult> {
|
|
148
|
-
const docDir = getDocumentDirectory();
|
|
149
|
-
const uniqueFilename = FileUtils.generateUniqueFilename(
|
|
150
|
-
filename || sourceUri.split("/").pop() || "file"
|
|
151
|
-
);
|
|
152
|
-
const destinationUri = FileUtils.joinPaths(docDir, uniqueFilename);
|
|
153
|
-
return copyFile(sourceUri, destinationUri);
|
|
17
|
+
static readFile = readFile;
|
|
18
|
+
static readFileAsBase64 = readFileAsBase64;
|
|
19
|
+
static writeFile = writeFile;
|
|
20
|
+
static deleteFile = deleteFile;
|
|
21
|
+
static copyFile = copyFile;
|
|
22
|
+
static moveFile = moveFile;
|
|
23
|
+
static createDirectory = createDirectory;
|
|
24
|
+
static listDirectory = listDirectory;
|
|
25
|
+
static getDirectoryPath = getDirectoryPath;
|
|
26
|
+
static getDocumentDirectory = getDocumentDirectory;
|
|
27
|
+
static getCacheDirectory = getCacheDirectory;
|
|
28
|
+
static getFileInfo = getFileInfo;
|
|
29
|
+
static exists = fileExists;
|
|
30
|
+
static getFileSize = getFileSize;
|
|
31
|
+
static downloadFile = downloadFile;
|
|
32
|
+
static clearCache = clearCache;
|
|
33
|
+
static getDirectorySize = getDirectorySize;
|
|
34
|
+
static generateFilePath = generateFilePath;
|
|
35
|
+
|
|
36
|
+
static async copyToCache(sourceUri: string, filename?: string): Promise<FileOperationResult> {
|
|
37
|
+
const dest = FileUtils.joinPaths(getCacheDirectory(), FileUtils.generateUniqueFilename(filename || sourceUri.split("/").pop() || "file"));
|
|
38
|
+
return copyFile(sourceUri, dest);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static async copyToDocuments(sourceUri: string, filename?: string): Promise<FileOperationResult> {
|
|
42
|
+
const dest = FileUtils.joinPaths(getDocumentDirectory(), FileUtils.generateUniqueFilename(filename || sourceUri.split("/").pop() || "file"));
|
|
43
|
+
return copyFile(sourceUri, dest);
|
|
154
44
|
}
|
|
155
45
|
}
|
|
@@ -21,7 +21,7 @@ export async function clearCache(): Promise<boolean> {
|
|
|
21
21
|
files.map((file) => deleteFile(FileUtils.joinPaths(cacheDir, file))),
|
|
22
22
|
);
|
|
23
23
|
return true;
|
|
24
|
-
} catch
|
|
24
|
+
} catch {
|
|
25
25
|
return false;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
@@ -40,7 +40,7 @@ export async function getDirectorySize(uri: string): Promise<number> {
|
|
|
40
40
|
}),
|
|
41
41
|
);
|
|
42
42
|
return sizes.reduce((total, size) => total + size, 0);
|
|
43
|
-
} catch
|
|
43
|
+
} catch {
|
|
44
44
|
return 0;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -14,7 +14,7 @@ export async function createDirectory(uri: string): Promise<boolean> {
|
|
|
14
14
|
const dir = new Directory(uri);
|
|
15
15
|
dir.create({ intermediates: true, idempotent: true });
|
|
16
16
|
return true;
|
|
17
|
-
} catch
|
|
17
|
+
} catch {
|
|
18
18
|
return false;
|
|
19
19
|
}
|
|
20
20
|
}
|
|
@@ -27,7 +27,7 @@ export async function listDirectory(uri: string): Promise<string[]> {
|
|
|
27
27
|
const dir = new Directory(uri);
|
|
28
28
|
const items = dir.list();
|
|
29
29
|
return items.map((item) => item.uri);
|
|
30
|
-
} catch
|
|
30
|
+
} catch {
|
|
31
31
|
return [];
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -45,7 +45,7 @@ export function getDirectoryPath(type: DirectoryType): string {
|
|
|
45
45
|
default:
|
|
46
46
|
return "";
|
|
47
47
|
}
|
|
48
|
-
} catch
|
|
48
|
+
} catch {
|
|
49
49
|
return "";
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -1,207 +1,115 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Download Service
|
|
3
|
-
* Single Responsibility:
|
|
3
|
+
* Single Responsibility: Handle file download operations
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { File, Paths, Directory } from "expo-file-system";
|
|
7
|
-
import type { FileOperationResult
|
|
7
|
+
import type { FileOperationResult } from "../../domain/entities/File";
|
|
8
8
|
import { FileUtils } from "../../domain/entities/File";
|
|
9
|
+
import type { DownloadProgressCallback, DownloadWithProgressResult } from "./download.types";
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
const hashUrl = (url: string): string => {
|
|
12
|
+
let hash = 0;
|
|
13
|
+
for (let i = 0; i < url.length; i++) hash = ((hash << 5) - hash + url.charCodeAt(i)) | 0;
|
|
14
|
+
return Math.abs(hash).toString(36);
|
|
15
|
+
};
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
+
const getExt = (url: string): string => {
|
|
18
|
+
const ext = url.split("?")[0].split(".").pop()?.toLowerCase() || "mp4";
|
|
19
|
+
return ["mp4", "mov", "m4v", "webm", "jpg", "png", "pdf"].includes(ext) ? ext : "mp4";
|
|
20
|
+
};
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export async function downloadFile(
|
|
22
|
-
url: string,
|
|
23
|
-
destinationUri?: string,
|
|
24
|
-
): Promise<FileOperationResult> {
|
|
25
|
-
try {
|
|
26
|
-
let destination: File | typeof Paths.document;
|
|
27
|
-
|
|
28
|
-
if (destinationUri) {
|
|
29
|
-
destination = new File(destinationUri);
|
|
30
|
-
} else {
|
|
31
|
-
const filename = FileUtils.generateUniqueFilename("download");
|
|
32
|
-
destination = new File(Paths.document, filename);
|
|
33
|
-
}
|
|
22
|
+
const getCacheUri = (url: string, dir: string): string => {
|
|
23
|
+
return FileUtils.joinPaths(dir, `cached_${hashUrl(url)}.${getExt(url)}`);
|
|
24
|
+
};
|
|
34
25
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
38
|
-
return { success: true, uri: result.uri };
|
|
39
|
-
} catch (error) {
|
|
40
|
-
return {
|
|
41
|
-
success: false,
|
|
42
|
-
error: error instanceof Error ? error.message : "Download failed",
|
|
43
|
-
};
|
|
44
|
-
}
|
|
26
|
+
interface DownloadError extends Error {
|
|
27
|
+
message: string;
|
|
45
28
|
}
|
|
46
29
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
30
|
+
export async function downloadFile(url: string, dest?: string): Promise<FileOperationResult> {
|
|
31
|
+
try {
|
|
32
|
+
const destination = dest
|
|
33
|
+
? new File(dest)
|
|
34
|
+
: new File(Paths.document, FileUtils.generateUniqueFilename("download"));
|
|
35
|
+
const res = await File.downloadFileAsync(url, destination, { idempotent: true });
|
|
36
|
+
return { success: true, uri: res.uri };
|
|
37
|
+
} catch (error) {
|
|
38
|
+
const downloadError = error as DownloadError;
|
|
39
|
+
return { success: false, error: downloadError.message || "Unknown error" };
|
|
56
40
|
}
|
|
57
|
-
return Math.abs(hash).toString(36);
|
|
58
41
|
}
|
|
59
42
|
|
|
60
|
-
/**
|
|
61
|
-
* Get extension from URL
|
|
62
|
-
*/
|
|
63
|
-
function getExtensionFromUrl(url: string): string {
|
|
64
|
-
const urlPath = url.split("?")[0];
|
|
65
|
-
const ext = urlPath.split(".").pop()?.toLowerCase() || "mp4";
|
|
66
|
-
const validExts = ["mp4", "mov", "m4v", "webm", "jpg", "png", "pdf"];
|
|
67
|
-
return validExts.includes(ext) ? ext : "mp4";
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Download file with progress tracking (uses expo/fetch for progress)
|
|
72
|
-
* Falls back to simple download if progress not needed
|
|
73
|
-
*/
|
|
74
43
|
export async function downloadFileWithProgress(
|
|
75
44
|
url: string,
|
|
76
45
|
cacheDir: string,
|
|
77
46
|
onProgress?: DownloadProgressCallback,
|
|
78
47
|
): Promise<DownloadWithProgressResult> {
|
|
79
48
|
try {
|
|
80
|
-
// Ensure cache directory exists
|
|
81
49
|
const dir = new Directory(cacheDir);
|
|
82
50
|
if (!dir.exists) {
|
|
83
51
|
dir.create({ intermediates: true, idempotent: true });
|
|
84
52
|
}
|
|
85
53
|
|
|
86
|
-
|
|
87
|
-
const hash = hashUrl(url);
|
|
88
|
-
const ext = getExtensionFromUrl(url);
|
|
89
|
-
const filename = `cached_${hash}.${ext}`;
|
|
90
|
-
const destUri = FileUtils.joinPaths(cacheDir, filename);
|
|
91
|
-
|
|
92
|
-
// Check if already cached
|
|
54
|
+
const destUri = getCacheUri(url, cacheDir);
|
|
93
55
|
const cachedFile = new File(destUri);
|
|
94
|
-
if (cachedFile.exists
|
|
56
|
+
if (cachedFile.exists) {
|
|
95
57
|
return { success: true, uri: destUri, fromCache: true };
|
|
96
58
|
}
|
|
97
59
|
|
|
98
|
-
// Download using fetch with progress (expo/fetch)
|
|
99
60
|
const response = await fetch(url);
|
|
100
|
-
|
|
101
61
|
if (!response.ok) {
|
|
102
|
-
throw new Error(`HTTP ${response.status}
|
|
62
|
+
throw new Error(`HTTP ${response.status}`);
|
|
103
63
|
}
|
|
104
64
|
|
|
105
|
-
const
|
|
106
|
-
const totalBytes = contentLength ? parseInt(contentLength, 10) : 0;
|
|
107
|
-
|
|
65
|
+
const totalBytes = parseInt(response.headers.get("content-length") || "0", 10);
|
|
108
66
|
if (!response.body) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return { ...result, fromCache: false };
|
|
67
|
+
const downloadResult = await downloadFile(url, destUri);
|
|
68
|
+
return { ...downloadResult, fromCache: false };
|
|
112
69
|
}
|
|
113
70
|
|
|
114
|
-
// Stream download with progress
|
|
115
71
|
const reader = response.body.getReader();
|
|
116
72
|
const chunks: Uint8Array[] = [];
|
|
117
|
-
let
|
|
73
|
+
let received = 0;
|
|
118
74
|
|
|
119
75
|
while (true) {
|
|
120
76
|
const { done, value } = await reader.read();
|
|
121
77
|
if (done) break;
|
|
122
|
-
|
|
123
78
|
chunks.push(value);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
totalBytesExpectedToWrite: totalBytes || receivedBytes,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
79
|
+
received += value.length;
|
|
80
|
+
onProgress?.({
|
|
81
|
+
totalBytesWritten: received,
|
|
82
|
+
totalBytesExpectedToWrite: totalBytes || received,
|
|
83
|
+
});
|
|
132
84
|
}
|
|
133
85
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
let position = 0;
|
|
86
|
+
const all = new Uint8Array(received);
|
|
87
|
+
let pos = 0;
|
|
137
88
|
for (const chunk of chunks) {
|
|
138
|
-
|
|
139
|
-
|
|
89
|
+
all.set(chunk, pos);
|
|
90
|
+
pos += chunk.length;
|
|
140
91
|
}
|
|
141
|
-
|
|
142
|
-
// Write bytes to file
|
|
143
|
-
const file = new File(destUri);
|
|
144
|
-
file.write(allChunks);
|
|
92
|
+
new File(destUri).write(all);
|
|
145
93
|
|
|
146
94
|
return { success: true, uri: destUri, fromCache: false };
|
|
147
95
|
} catch (error) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
error: error instanceof Error ? error.message : "Download failed",
|
|
151
|
-
};
|
|
96
|
+
const downloadError = error as DownloadError;
|
|
97
|
+
return { success: false, error: downloadError.message || "Unknown error" };
|
|
152
98
|
}
|
|
153
99
|
}
|
|
154
100
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
export function isUrlCached(url: string, cacheDir: string): boolean {
|
|
159
|
-
try {
|
|
160
|
-
const hash = hashUrl(url);
|
|
161
|
-
const ext = getExtensionFromUrl(url);
|
|
162
|
-
const filename = `cached_${hash}.${ext}`;
|
|
163
|
-
const destUri = FileUtils.joinPaths(cacheDir, filename);
|
|
164
|
-
const file = new File(destUri);
|
|
165
|
-
return file.exists && (file.size ?? 0) > 0;
|
|
166
|
-
} catch {
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
101
|
+
export const isUrlCached = (url: string, dir: string): boolean => {
|
|
102
|
+
return new File(getCacheUri(url, dir)).exists;
|
|
103
|
+
};
|
|
170
104
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
export function getCachedFileUri(url: string, cacheDir: string): string | null {
|
|
175
|
-
try {
|
|
176
|
-
const hash = hashUrl(url);
|
|
177
|
-
const ext = getExtensionFromUrl(url);
|
|
178
|
-
const filename = `cached_${hash}.${ext}`;
|
|
179
|
-
const destUri = FileUtils.joinPaths(cacheDir, filename);
|
|
180
|
-
const file = new File(destUri);
|
|
181
|
-
if (file.exists && (file.size ?? 0) > 0) {
|
|
182
|
-
return destUri;
|
|
183
|
-
}
|
|
184
|
-
return null;
|
|
185
|
-
} catch {
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
105
|
+
export const getCachedFileUri = (url: string, dir: string): string | null => {
|
|
106
|
+
return isUrlCached(url, dir) ? getCacheUri(url, dir) : null;
|
|
107
|
+
};
|
|
189
108
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
const hash = hashUrl(url);
|
|
196
|
-
const ext = getExtensionFromUrl(url);
|
|
197
|
-
const filename = `cached_${hash}.${ext}`;
|
|
198
|
-
const destUri = FileUtils.joinPaths(cacheDir, filename);
|
|
199
|
-
const file = new File(destUri);
|
|
200
|
-
if (file.exists) {
|
|
201
|
-
file.delete();
|
|
202
|
-
}
|
|
203
|
-
return true;
|
|
204
|
-
} catch {
|
|
205
|
-
return false;
|
|
109
|
+
export const deleteCachedFile = (url: string, dir: string): boolean => {
|
|
110
|
+
const file = new File(getCacheUri(url, dir));
|
|
111
|
+
if (file.exists) {
|
|
112
|
+
file.delete();
|
|
206
113
|
}
|
|
207
|
-
|
|
114
|
+
return true;
|
|
115
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FileOperationResult, DownloadProgress } from "../../domain/entities/File";
|
|
2
|
+
|
|
3
|
+
export type DownloadProgressCallback = (progress: DownloadProgress) => void;
|
|
4
|
+
|
|
5
|
+
export interface DownloadWithProgressResult extends FileOperationResult {
|
|
6
|
+
fromCache?: boolean;
|
|
7
|
+
}
|
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
|
|
6
6
|
import type { FileEncoding } from "../../domain/entities/File";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Encoding type for Expo FileSystem
|
|
10
|
+
*/
|
|
11
|
+
export type ExpoEncodingType = FileEncoding;
|
|
12
|
+
|
|
8
13
|
/**
|
|
9
14
|
* Convert FileEncoding to Expo FileSystem encoding type
|
|
10
|
-
* Legacy API uses EncodingType enum
|
|
11
15
|
*/
|
|
12
|
-
export function getEncodingType(encoding: FileEncoding):
|
|
13
|
-
// Legacy API uses EncodingType enum
|
|
14
|
-
// Return as string for compatibility
|
|
16
|
+
export function getEncodingType(encoding: FileEncoding): ExpoEncodingType {
|
|
15
17
|
return encoding;
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -21,4 +23,3 @@ export function getEncodingType(encoding: FileEncoding): any {
|
|
|
21
23
|
export function isValidEncoding(encoding: string): encoding is FileEncoding {
|
|
22
24
|
return encoding === "utf8" || encoding === "base64";
|
|
23
25
|
}
|
|
24
|
-
|
|
@@ -25,7 +25,7 @@ export async function getFileInfo(uri: string): Promise<FileInfo | null> {
|
|
|
25
25
|
isDirectory: false,
|
|
26
26
|
modificationTime: file.modificationTime || 0,
|
|
27
27
|
};
|
|
28
|
-
} catch
|
|
28
|
+
} catch {
|
|
29
29
|
return null;
|
|
30
30
|
}
|
|
31
31
|
}
|
|
@@ -37,7 +37,7 @@ export async function fileExists(uri: string): Promise<boolean> {
|
|
|
37
37
|
try {
|
|
38
38
|
const file = new File(uri);
|
|
39
39
|
return file.exists;
|
|
40
|
-
} catch
|
|
40
|
+
} catch {
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { File } from "expo-file-system";
|
|
7
7
|
import type { FileEncoding } from "../../domain/entities/File";
|
|
8
|
-
|
|
8
|
+
import { blobToBase64 } from "../utils/blob.utils";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Read file as string with encoding
|
|
@@ -26,7 +26,7 @@ export async function readFile(
|
|
|
26
26
|
}
|
|
27
27
|
return await response.text();
|
|
28
28
|
}
|
|
29
|
-
} catch
|
|
29
|
+
} catch {
|
|
30
30
|
// Fall through to FileSystem API
|
|
31
31
|
}
|
|
32
32
|
}
|
|
@@ -39,33 +39,14 @@ export async function readFile(
|
|
|
39
39
|
}
|
|
40
40
|
const content = await file.text();
|
|
41
41
|
return content;
|
|
42
|
-
} catch
|
|
42
|
+
} catch {
|
|
43
43
|
return null;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/**
|
|
48
|
-
* Convert blob to base64 string
|
|
49
|
-
*/
|
|
50
|
-
function blobToBase64(blob: Blob): Promise<string> {
|
|
51
|
-
return new Promise((resolve, reject) => {
|
|
52
|
-
const reader = new FileReader();
|
|
53
|
-
reader.onloadend = () => {
|
|
54
|
-
const result = reader.result as string;
|
|
55
|
-
// Remove data URL prefix if present
|
|
56
|
-
const base64 = result.includes(",") ? result.split(",")[1] : result;
|
|
57
|
-
resolve(base64);
|
|
58
|
-
};
|
|
59
|
-
reader.onerror = reject;
|
|
60
|
-
reader.readAsDataURL(blob);
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
47
|
/**
|
|
65
48
|
* Read file as base64 string
|
|
66
49
|
*/
|
|
67
50
|
export async function readFileAsBase64(uri: string): Promise<string | null> {
|
|
68
51
|
return readFile(uri, "base64");
|
|
69
52
|
}
|
|
70
|
-
|
|
71
|
-
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { File } from "expo-file-system";
|
|
7
7
|
import type { FileEncoding, FileOperationResult } from "../../domain/entities/File";
|
|
8
|
-
import { getEncodingType } from "./encoding.service";
|
|
8
|
+
import { getEncodingType, type ExpoEncodingType } from "./encoding.service";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Write string to file
|
|
@@ -19,14 +19,14 @@ export async function writeFile(
|
|
|
19
19
|
const encodingType = getEncodingType(encoding);
|
|
20
20
|
const file = new File(uri);
|
|
21
21
|
file.write(content, {
|
|
22
|
-
encoding: encodingType as
|
|
22
|
+
encoding: encodingType as ExpoEncodingType,
|
|
23
23
|
});
|
|
24
24
|
return { success: true, uri };
|
|
25
25
|
} catch (error) {
|
|
26
|
+
const writeError = error as Error;
|
|
26
27
|
return {
|
|
27
28
|
success: false,
|
|
28
|
-
error:
|
|
29
|
+
error: writeError.message || "Unknown error",
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
|
-
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blob Utils
|
|
3
|
+
* Blob utility functions for file operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Convert blob to base64 string
|
|
8
|
+
*/
|
|
9
|
+
export function blobToBase64(blob: Blob): Promise<string> {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const reader = new FileReader();
|
|
12
|
+
reader.onloadend = () => {
|
|
13
|
+
const result = reader.result as string;
|
|
14
|
+
// Remove data URL prefix if present
|
|
15
|
+
const base64 = result.includes(",") ? result.split(",")[1] : result;
|
|
16
|
+
resolve(base64);
|
|
17
|
+
};
|
|
18
|
+
reader.onerror = reject;
|
|
19
|
+
reader.readAsDataURL(blob);
|
|
20
|
+
});
|
|
21
|
+
}
|