[client][web] Upload folders (where supported)

This commit is contained in:
Abhishek Shroff
2026-01-16 21:11:30 +05:30
parent 276473affe
commit 04d1c2b621
2 changed files with 67 additions and 33 deletions

View File

@@ -1,38 +1,76 @@
import 'dart:async';
import 'dart:js_interop';
import 'dart:js_interop_unsafe';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:phylum/libphylum/repositories/resource_repository.dart';
import 'package:phylum/util/upload_utils.dart';
import 'package:web/web.dart' as web;
bool get folderUploadSupported => true;
bool _folderUploadSupported = web.window.has('showDirectoryPicker');
bool get folderUploadSupported => _folderUploadSupported;
@JS()
external JSPromise<web.FileSystemDirectoryHandle> showDirectoryPicker();
Future<void> pickAndUploadDirectory(BuildContext context, String parent) async {
final dir = await showDirectoryPicker().toDart;
processDirs(dir);
Future<void> pickAndUploadDirectory(BuildContext context, String folderId) async {
final dir =
await (web.window.callMethod('showDirectoryPicker'.toJS) as JSPromise<web.FileSystemDirectoryHandle>).toDart;
final tree = await enumerateDir(dir, '');
uploadDirTree(context, folderId, tree);
}
Future<void> processDirs(web.FileSystemDirectoryHandle dir) async {
print('Processing ${dir.name}');
final it = dir.entries().asStream();
it.forEach((e) async {
final l = e.toDart;
Future<DirTree> enumerateDir(web.FileSystemDirectoryHandle dir, String prefix) async {
if (prefix.isNotEmpty) prefix += '/';
final files = <UploadFile>[];
final dirs = <String>['$prefix${dir.name}'];
int totalSize = 0;
if (l[1].isA<web.FileSystemDirectoryHandle>()) {
await processDirs(l[1] as web.FileSystemDirectoryHandle);
} else if (l[1].isA<web.FileSystemFileHandle>()) {
final file = await (l[1] as web.FileSystemFileHandle).getFile().toDart;
final reader = web.FileReader();
reader.onload = () {
// final bytes = (reader.result as JSArrayBuffer).toDart;
print('contents of ${file.name}: ${reader.result}');
}.toJS;
final entries = dir.entries();
while (true) {
final result = await entries.next().toDart;
if (result.done) break;
final handle = result.value.toDart;
reader.readAsText(file);
// reader.readAsArrayBuffer(file);
if (handle[1].isA<web.FileSystemDirectoryHandle>()) {
final subtree = await enumerateDir(handle[1] as web.FileSystemDirectoryHandle, '$prefix${dir.name}');
dirs.addAll(subtree.dirs);
files.addAll(subtree.files);
totalSize += subtree.totalSize;
} else if (handle[1].isA<web.FileSystemFileHandle>()) {
final f = handle[1] as web.FileSystemFileHandle;
final file = await f.getFile().toDart;
totalSize += file.size;
files.add(UploadFile(
size: file.size,
contentType: getMimeType(file.name),
byteStream: ByteStream(byteStream(file)),
path: '$prefix${dir.name}/${file.name}'));
}
});
}
;
return DirTree(
totalSize: totalSize,
files: files,
dirs: dirs,
);
}
Stream<Uint8List> byteStream(web.File file) async* {
// Hack: wait to allocate the array buffer until after the first yield
yield Uint8List(0);
const int size = 64 * 1024;
final stream = file.stream();
final reader = stream.getReader(web.ReadableStreamGetReaderOptions(mode: "byob")) as web.ReadableStreamBYOBReader;
final buffer = JSArrayBuffer(size);
final view = JSUint8Array(buffer, 0, size);
var result = await reader.read(view).toDart;
while (!result.done) {
final buf = result.value.dartify() as Uint8List;
yield buf;
result = await reader.read(buf.toJS).toDart;
}
}
extension on web.FileSystemDirectoryHandle {
@@ -41,14 +79,6 @@ extension on web.FileSystemDirectoryHandle {
extension type JsAsyncIterator<T extends JSAny>._(JSObject _) implements JSObject {
external JSPromise<JsAsyncIteratorState<T>> next();
Stream<T> asStream() async* {
while (true) {
final result = await next().toDart;
if (result.done) break;
yield result.value;
}
}
}
extension type JsAsyncIteratorState<T extends JSAny>._(JSObject _) implements JSObject {

View File

@@ -109,7 +109,6 @@ Future<void> uploadFiles(BuildContext context, String folderId, Iterable<UploadF
}
Future<void> uploadPath(BuildContext context, String folderId, Iterable<String> paths) async {
final account = context.read<PhylumAccount>();
showProgressDialog(context, message: 'Counting Files');
DirTree? tree;
for (final path in paths) {
@@ -121,6 +120,11 @@ Future<void> uploadPath(BuildContext context, String folderId, Iterable<String>
Navigator.of(context).pop();
if (tree == null) return;
return uploadDirTree(context, folderId, tree);
}
Future<void> uploadDirTree(BuildContext context, String folderId, DirTree tree) async {
final account = context.read<PhylumAccount>();
final confirm = await showAlertDialog(
context,
title: 'Upload Files?',