Skip to content

Commit c3fd550

Browse files
author
Amine
committed
refactor: add ExpoFileSystemAdapter for local storage management in React Native
1 parent be856e9 commit c3fd550

File tree

2 files changed

+115
-0
lines changed

2 files changed

+115
-0
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import * as FileSystem from 'expo-file-system';
2+
import { decode as decodeBase64, encode as encodeBase64 } from 'base64-arraybuffer';
3+
import { AttachmentData, EncodingType, LocalStorageAdapter } from '@powersync/common';
4+
5+
/**
6+
* ExpoFileSystemAdapter implements LocalStorageAdapter using Expo's FileSystem.
7+
* Suitable for React Native applications using Expo or Expo modules.
8+
*/
9+
export class ExpoFileSystemAdapter implements LocalStorageAdapter {
10+
private storageDirectory: string;
11+
12+
constructor(storageDirectory?: string) {
13+
// Default to a subdirectory in the document directory
14+
this.storageDirectory = storageDirectory ?? `${FileSystem.documentDirectory}attachments/`;
15+
}
16+
17+
async initialize(): Promise<void> {
18+
const dirInfo = await FileSystem.getInfoAsync(this.storageDirectory);
19+
if (!dirInfo.exists) {
20+
await FileSystem.makeDirectoryAsync(this.storageDirectory, { intermediates: true });
21+
}
22+
}
23+
24+
async clear(): Promise<void> {
25+
const dirInfo = await FileSystem.getInfoAsync(this.storageDirectory);
26+
if (dirInfo.exists) {
27+
await FileSystem.deleteAsync(this.storageDirectory);
28+
}
29+
}
30+
31+
getLocalUri(filename: string): string {
32+
return `${this.storageDirectory}${filename}`;
33+
}
34+
35+
async saveFile(
36+
filePath: string,
37+
data: AttachmentData,
38+
options?: { encoding?: EncodingType; mediaType?: string }
39+
): Promise<number> {
40+
let size: number;
41+
42+
if (typeof data === 'string') {
43+
// Handle string data (typically base64 or UTF8)
44+
const encoding = options?.encoding ?? EncodingType.Base64;
45+
await FileSystem.writeAsStringAsync(filePath, data, {
46+
encoding: encoding === EncodingType.Base64 ? FileSystem.EncodingType.Base64 : FileSystem.EncodingType.UTF8
47+
});
48+
49+
// Calculate size based on encoding
50+
if (encoding === EncodingType.Base64) {
51+
// Base64 string length / 4 * 3 gives approximate byte size
52+
size = Math.ceil((data.length / 4) * 3);
53+
} else {
54+
// UTF8: Use TextEncoder to get accurate byte count
55+
const encoder = new TextEncoder();
56+
size = encoder.encode(data).byteLength;
57+
}
58+
} else {
59+
// Handle ArrayBuffer data
60+
const base64 = encodeBase64(data);
61+
await FileSystem.writeAsStringAsync(filePath, base64, {
62+
encoding: FileSystem.EncodingType.Base64
63+
});
64+
size = data.byteLength;
65+
}
66+
67+
return size;
68+
}
69+
70+
async readFile(filePath: string, options?: { encoding?: EncodingType; mediaType?: string }): Promise<ArrayBuffer> {
71+
const encoding = options?.encoding ?? EncodingType.Base64;
72+
73+
// Let the native function throw if file doesn't exist
74+
const content = await FileSystem.readAsStringAsync(filePath, {
75+
encoding: encoding === EncodingType.Base64 ? FileSystem.EncodingType.Base64 : FileSystem.EncodingType.UTF8
76+
});
77+
78+
if (encoding === EncodingType.UTF8) {
79+
// Convert UTF8 string to ArrayBuffer
80+
const encoder = new TextEncoder();
81+
return encoder.encode(content).buffer;
82+
} else {
83+
// Convert base64 string to ArrayBuffer
84+
return decodeBase64(content);
85+
}
86+
}
87+
88+
async deleteFile(filePath: string, options?: { filename?: string }): Promise<void> {
89+
await FileSystem.deleteAsync(filePath).catch((error: any) => {
90+
// Gracefully ignore file not found errors, throw others
91+
if (error?.message?.includes('not exist') || error?.message?.includes('ENOENT')) {
92+
return;
93+
}
94+
throw error;
95+
});
96+
}
97+
98+
async fileExists(filePath: string): Promise<boolean> {
99+
try {
100+
const info = await FileSystem.getInfoAsync(filePath);
101+
return info.exists;
102+
} catch {
103+
return false;
104+
}
105+
}
106+
107+
async makeDir(path: string): Promise<void> {
108+
await FileSystem.makeDirectoryAsync(path, { intermediates: true });
109+
}
110+
111+
async rmDir(path: string): Promise<void> {
112+
await FileSystem.deleteAsync(path);
113+
}
114+
}

packages/react-native/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from './db/adapters/react-native-quick-sqlite/RNQSDBOpenFactory';
88
export * from './sync/stream/ReactNativeRemote';
99
export * from './sync/stream/ReactNativeStreamingSyncImplementation';
1010
export * from './db/adapters/react-native-quick-sqlite/ReactNativeQuickSQLiteOpenFactory';
11+
export * from './attachments/ExpoFileSystemAdapter';

0 commit comments

Comments
 (0)