@umituz/react-native-filesystem 2.1.15 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-filesystem",
3
- "version": "2.1.15",
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 types and utilities for file operations
2
+ * File Entity - Module Index
3
+ * Domain layer exports for file operations
4
4
  */
5
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
- }
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
- * Get filename from URI
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
- * Validate file size
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
- DownloadProgressCallback,
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
- // File Reading
33
- static async readFile(
34
- uri: string,
35
- encoding: FileEncoding = "utf8"
36
- ): Promise<string | null> {
37
- return readFile(uri, encoding);
38
- }
39
-
40
- static async readFileAsBase64(uri: string): Promise<string | null> {
41
- return readFileAsBase64(uri);
42
- }
43
-
44
- // File Writing
45
- static async writeFile(
46
- uri: string,
47
- content: string,
48
- encoding: FileEncoding = "utf8"
49
- ): Promise<FileOperationResult> {
50
- return writeFile(uri, content, encoding);
51
- }
52
-
53
- // File Management
54
- static async deleteFile(uri: string): Promise<boolean> {
55
- return deleteFile(uri);
56
- }
57
-
58
- static async copyFile(
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 (error) {
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 (error) {
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 (error) {
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 (error) {
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 (error) {
48
+ } catch {
49
49
  return "";
50
50
  }
51
51
  }
@@ -1,207 +1,115 @@
1
1
  /**
2
2
  * Download Service
3
- * Single Responsibility: Download files from URLs with progress tracking
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, DownloadProgress } from "../../domain/entities/File";
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
- /** Progress callback type */
11
- export type DownloadProgressCallback = (progress: DownloadProgress) => void;
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
- /** Download with progress result */
14
- export interface DownloadWithProgressResult extends FileOperationResult {
15
- fromCache?: boolean;
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
- * Download file from URL (simple version without progress)
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
- const result = await File.downloadFileAsync(url, destination, {
36
- idempotent: true,
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
- * Generate hash from URL for cache filename
49
- */
50
- function hashUrl(url: string): string {
51
- let hash = 0;
52
- for (let i = 0; i < url.length; i++) {
53
- const char = url.charCodeAt(i);
54
- hash = (hash << 5) - hash + char;
55
- hash = hash & hash;
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
- // Generate filename from URL hash
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 && (cachedFile.size ?? 0) > 0) {
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}: ${response.statusText}`);
62
+ throw new Error(`HTTP ${response.status}`);
103
63
  }
104
64
 
105
- const contentLength = response.headers.get("content-length");
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
- // Fallback: no streaming support, use simple download
110
- const result = await downloadFile(url, destUri);
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 receivedBytes = 0;
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
- receivedBytes += value.length;
125
-
126
- if (onProgress) {
127
- onProgress({
128
- totalBytesWritten: receivedBytes,
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
- // Combine chunks and write to file
135
- const allChunks = new Uint8Array(receivedBytes);
136
- let position = 0;
86
+ const all = new Uint8Array(received);
87
+ let pos = 0;
137
88
  for (const chunk of chunks) {
138
- allChunks.set(chunk, position);
139
- position += chunk.length;
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
- return {
149
- success: false,
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
- * Check if URL is cached
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
- * Get cached file URI if exists
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
- * Delete cached file
192
- */
193
- export function deleteCachedFile(url: string, cacheDir: string): boolean {
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): any {
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 (error) {
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 (error) {
40
+ } catch {
41
41
  return false;
42
42
  }
43
43
  }
@@ -34,7 +34,7 @@ export async function deleteFile(uri: string): Promise<boolean> {
34
34
  }
35
35
 
36
36
  return false;
37
- } catch (error) {
37
+ } catch {
38
38
  return false;
39
39
  }
40
40
  }
@@ -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 (fetchError) {
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 (error) {
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 any,
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: error instanceof Error ? error.message : "Unknown 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
+ }