Skip to content

Commit 4cb5753

Browse files
author
Amine
committed
feat: added ExpoFileSystemAdapter to the attachments package
1 parent f6b2343 commit 4cb5753

File tree

6 files changed

+9797
-6902
lines changed

6 files changed

+9797
-6902
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
"test": "pnpm run -r --workspace-concurrency=1 test --run",
2424
"test:packages:exports": "pnpm -r --filter {./packages/**} test:exports"
2525
},
26+
"pnpm": {
27+
"onlyBuiltDependencies": [
28+
"@powersync/better-sqlite3"
29+
]
30+
},
2631
"keywords": [],
2732
"type": "module",
2833
"author": "JOURNEYAPPS",

packages/attachments/package.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,17 @@
4545
"test:exports": "attw --pack ."
4646
},
4747
"peerDependencies": {
48-
"@powersync/common": "workspace:^1.40.0"
48+
"@powersync/common": "workspace:^1.40.0",
49+
"expo-file-system": "^18.0.0",
50+
"base64-arraybuffer": "^1.0.0"
51+
},
52+
"peerDependenciesMeta": {
53+
"expo-file-system": {
54+
"optional": true
55+
},
56+
"base64-arraybuffer": {
57+
"optional": true
58+
}
4959
},
5060
"devDependencies": {
5161
"@powersync/common": "workspace:*",

packages/attachments/rollup.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export default (commandLineArgs) => {
2828
sourceMap
2929
})
3030
],
31-
external: ['@powersync/common']
31+
external: ['@powersync/common', 'expo-file-system', 'base64-arraybuffer']
3232
},
3333
// This is required to avoid https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseESM.md
3434
{

packages/attachments/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './Schema.js';
22
export * from './LocalStorageAdapter.js';
33
export * from './storageAdapters/NodeFileSystemAdapter.js';
44
export * from './storageAdapters/IndexDBFileSystemAdapter.js';
5+
export * from './storageAdapters/ExpoFileSystemAdapter.js';
56
export * from './RemoteStorageAdapter.js';
67
export * from './AttachmentContext.js';
78
export * from './SyncingService.js';
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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 '../LocalStorageAdapter.js';
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+
}
115+

0 commit comments

Comments
 (0)