Skip to content

Commit 1172e8a

Browse files
committed
Add OPFS file system
1 parent 3ce5ea2 commit 1172e8a

File tree

3 files changed

+469
-2
lines changed

3 files changed

+469
-2
lines changed

packages/powersync_core/lib/src/attachments/storage/local_storage.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ abstract interface class LocalStorage {
3232
/// [filePath] - Path of the file to read
3333
///
3434
/// Returns a stream of binary data
35-
Stream<Uint8List> readFile(String filePath);
35+
Stream<Uint8List> readFile(String filePath, {String? mediaType});
3636

3737
/// Deletes a file at the specified path
3838
///
@@ -79,7 +79,7 @@ final class _InMemoryStorage implements LocalStorage {
7979
Future<void> initialize() async {}
8080

8181
@override
82-
Stream<Uint8List> readFile(String filePath) {
82+
Stream<Uint8List> readFile(String filePath, {String? mediaType}) {
8383
return switch (content[_keyForPath(filePath)]) {
8484
null =>
8585
Stream.error('file at $filePath does not exist in in-memory storage'),
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import 'dart:async';
2+
import 'dart:js_interop';
3+
import 'dart:js_interop_unsafe';
4+
import 'dart:typed_data';
5+
6+
import 'package:web/web.dart' as web;
7+
import 'package:path/path.dart' as p;
8+
import 'local_storage.dart';
9+
10+
/// A [LocalStorage] implementation suitable for the web, storing files in the
11+
/// [Origin private file system](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system).
12+
final class OpfsLocalStorage implements LocalStorage {
13+
final Future<web.FileSystemDirectoryHandle> Function() _root;
14+
Future<web.FileSystemDirectoryHandle>? _resolvedDirectory;
15+
16+
OpfsLocalStorage._(this._root);
17+
18+
/// Creates a [LocalStorage] implementation storing files in OPFS.
19+
///
20+
/// The [rootDirectory] acts as a chroot within `navigator.getDirectory()`,
21+
/// and allows storing attachments in a subdirectory.
22+
/// Users are strongly encouraged to set it, as [clear] would otherwise delete
23+
/// all of OFPS.
24+
factory OpfsLocalStorage(String rootDirectory) {
25+
return OpfsLocalStorage._(() async {
26+
var root = await _navigator.storage.getDirectory().toDart;
27+
for (final segment in p.url.split(rootDirectory)) {
28+
root = await root.getDirectory(segment, create: true);
29+
}
30+
31+
return root;
32+
});
33+
}
34+
35+
Future<web.FileSystemDirectoryHandle> get root {
36+
return _resolvedDirectory ??= _root();
37+
}
38+
39+
Future<(web.FileSystemDirectoryHandle, String)> _parentDirectoryAndName(
40+
String path,
41+
{bool create = false}) async {
42+
final segments = p.url.split(path);
43+
var dir = await root;
44+
for (var i = 0; i < segments.length - 1; i++) {
45+
dir = await dir.getDirectory(segments[i], create: create);
46+
}
47+
48+
return (dir, segments.last);
49+
}
50+
51+
Future<web.FileSystemFileHandle> _file(String path,
52+
{bool create = false}) async {
53+
final (parent, name) = await _parentDirectoryAndName(path, create: create);
54+
return await parent
55+
.getFileHandle(name, web.FileSystemGetFileOptions(create: create))
56+
.toDart;
57+
}
58+
59+
@override
60+
Future<void> clear() async {
61+
final dir = await root;
62+
await for (final entry in dir.values().toDart) {
63+
await dir.remove(entry.name, recursive: true);
64+
}
65+
}
66+
67+
@override
68+
Future<void> deleteFile(String filePath) async {
69+
try {
70+
final (parent, name) = await _parentDirectoryAndName(filePath);
71+
await parent.remove(name);
72+
} catch (e) {
73+
// Entry does not exist, skip.
74+
return;
75+
}
76+
}
77+
78+
@override
79+
Future<bool> fileExists(String filePath) async {
80+
try {
81+
await _file(filePath);
82+
return true;
83+
} catch (e) {
84+
// Entry does not exist, skip.
85+
return false;
86+
}
87+
}
88+
89+
@override
90+
Future<void> initialize() async {
91+
await root;
92+
}
93+
94+
@override
95+
Stream<Uint8List> readFile(String filePath, {String? mediaType}) async* {
96+
final file = await _file(filePath);
97+
final completer = Completer<Uint8List>.sync();
98+
final reader = web.FileReader();
99+
reader
100+
..onload = () {
101+
final data = (reader.result as JSArrayBuffer).toDart;
102+
completer.complete(data.asUint8List());
103+
}.toJS
104+
..onerror = () {
105+
completer.completeError(reader.error!);
106+
}.toJS;
107+
108+
reader.readAsArrayBuffer(await file.getFile().toDart);
109+
yield await completer.future;
110+
}
111+
112+
@override
113+
Future<int> saveFile(String filePath, Stream<List<int>> data) async {
114+
final file = await _file(filePath, create: true);
115+
final writable = await file.createWritable().toDart;
116+
117+
var bytesWritten = 0;
118+
await for (final chunk in data) {
119+
final asBuffer = switch (chunk) {
120+
final Uint8List blob => blob,
121+
_ => Uint8List.fromList(chunk),
122+
};
123+
124+
await writable.write(asBuffer.toJS).toDart;
125+
bytesWritten += asBuffer.length;
126+
}
127+
128+
await writable.close().toDart;
129+
return bytesWritten;
130+
}
131+
}
132+
133+
@JS('Symbol.asyncIterator')
134+
external JSSymbol get _asyncIterator;
135+
136+
@JS('navigator')
137+
external web.Navigator get _navigator;
138+
139+
extension FileSystemHandleApi on web.FileSystemHandle {
140+
bool get isFile => kind == 'file';
141+
142+
bool get isDirectory => kind == 'directory';
143+
}
144+
145+
extension FileSystemDirectoryHandleApi on web.FileSystemDirectoryHandle {
146+
Future<web.FileSystemFileHandle> openFile(String name,
147+
{bool create = false}) {
148+
return getFileHandle(name, web.FileSystemGetFileOptions(create: create))
149+
.toDart;
150+
}
151+
152+
Future<web.FileSystemDirectoryHandle> getDirectory(String name,
153+
{bool create = false}) {
154+
return getDirectoryHandle(
155+
name, web.FileSystemGetDirectoryOptions(create: create))
156+
.toDart;
157+
}
158+
159+
Future<void> remove(String name, {bool recursive = false}) {
160+
return removeEntry(name, web.FileSystemRemoveOptions(recursive: recursive))
161+
.toDart;
162+
}
163+
164+
external AsyncIterable<web.FileSystemHandle> values();
165+
}
166+
167+
extension type IteratorResult<T extends JSAny?>(JSObject _)
168+
implements JSObject {
169+
external JSBoolean? get done;
170+
external T? get value;
171+
}
172+
173+
extension type AsyncIterator<T extends JSAny?>(JSObject _) implements JSObject {
174+
external JSPromise<IteratorResult<T>> next();
175+
}
176+
177+
extension type AsyncIterable<T extends JSAny?>(JSObject _) implements JSObject {
178+
Stream<T> get toDart async* {
179+
final iterator = (getProperty(_asyncIterator) as JSFunction)
180+
.callAsFunction(this) as AsyncIterator<T>;
181+
182+
while (true) {
183+
final next = await iterator.next().toDart;
184+
if (next.done?.toDart == true) {
185+
break;
186+
}
187+
188+
yield next.value as T;
189+
}
190+
}
191+
}

0 commit comments

Comments
 (0)