Skip to content

Commit

Permalink
refactor(neon_files): Allow file upload/download operations on web
Browse files Browse the repository at this point in the history
Signed-off-by: provokateurin <[email protected]>
  • Loading branch information
provokateurin committed Feb 14, 2024
1 parent 3593924 commit 275555c
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 50 deletions.
74 changes: 56 additions & 18 deletions packages/neon/neon_files/lib/src/blocs/files.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import 'package:neon_files/src/options.dart';
import 'package:neon_files/src/utils/task.dart';
import 'package:neon_framework/blocs.dart';
import 'package:neon_framework/models.dart';
import 'package:neon_framework/platform.dart';
import 'package:neon_framework/utils.dart';
import 'package:nextcloud/webdav.dart';
import 'package:nextcloud/nextcloud.dart';
import 'package:open_file/open_file.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
Expand All @@ -31,9 +32,11 @@ sealed class FilesBloc implements InteractiveBloc {

void uploadFile(PathUri uri, String localPath);

void uploadMemory(PathUri uri, Uint8List bytes, {DateTime? lastModified});

void openFile(PathUri uri, String etag, String? mimeType);

void shareFileNative(PathUri uri, String etag);
void shareFileNative(PathUri uri, String etag, String? mimeType);

void delete(PathUri uri);

Expand Down Expand Up @@ -118,24 +121,31 @@ class _FilesBloc extends InteractiveBloc implements FilesBloc {
void openFile(PathUri uri, String etag, String? mimeType) {
wrapAction(
() async {
final file = await cacheFile(uri, etag);

final result = await OpenFile.open(file.path, type: mimeType);
if (result.type != ResultType.done) {
throw const UnableToOpenFileException();
if (NeonPlatform.instance.canUsePaths) {
final file = await cacheFile(uri, etag);
final result = await OpenFile.open(file.path, type: mimeType);
if (result.type != ResultType.done) {
throw const UnableToOpenFileException();
}
} else {
final bytes = await downloadMemory(uri);
await NeonPlatform.instance.saveFileWithPickDialog(uri.name, mimeType ?? 'application/octet-stream', bytes);
}
},
disableTimeout: true,
);
}

@override
void shareFileNative(PathUri uri, String etag) {
void shareFileNative(PathUri uri, String etag, String? mimeType) {
wrapAction(
() async {
final file = await cacheFile(uri, etag);

await Share.shareXFiles([XFile(file.path)]);
if (NeonPlatform.instance.canUsePaths) {
final file = await cacheFile(uri, etag);
await Share.shareXFiles([XFile(file.path)]);
} else {
throw UnimplementedError('Sharing is not supported on web');
}
},
disableTimeout: true,
);
Expand Down Expand Up @@ -170,7 +180,7 @@ class _FilesBloc extends InteractiveBloc implements FilesBloc {
void uploadFile(PathUri uri, String localPath) {
wrapAction(
() async {
final task = FilesUploadTask(
final task = FilesUploadTaskIO(
uri: uri,
file: File(localPath),
);
Expand All @@ -182,6 +192,24 @@ class _FilesBloc extends InteractiveBloc implements FilesBloc {
);
}

@override
void uploadMemory(PathUri uri, Uint8List bytes, {DateTime? lastModified}) {
wrapAction(
() async {
final task = FilesUploadTaskMemory(
uri: uri,
size: bytes.length,
lastModified: lastModified,
bytes: bytes,
);
tasks.add(tasks.value..add(task));
await uploadQueue.add(() => task.execute(account.client));
tasks.add(tasks.value..remove(task));
},
disableTimeout: true,
);
}

Future<File> cacheFile(PathUri uri, String etag) async {
final cacheDir = await getApplicationCacheDirectory();
final file = File(p.join(cacheDir.path, 'files', etag.replaceAll('"', ''), uri.name));
Expand All @@ -191,23 +219,33 @@ class _FilesBloc extends InteractiveBloc implements FilesBloc {
if (!file.parent.existsSync()) {
await file.parent.create(recursive: true);
}
await downloadFile(uri, file);
await downloadIO(uri, file);
}

return file;
}

Future<void> downloadFile(
PathUri uri,
File file,
) async {
final task = FilesDownloadTask(
Future<void> downloadIO(PathUri uri, File file) async {
final task = FilesDownloadTaskIO(
uri: uri,
file: file,
);

tasks.add(tasks.value..add(task));
await downloadQueue.add(() => task.execute(account.client));
tasks.add(tasks.value..remove(task));
}

Future<Uint8List> downloadMemory(PathUri uri) async {
final task = FilesDownloadTaskMemory(uri: uri);
// We need to listen to the stream, otherwise it will get stuck.
final future = task.stream.bytes;

tasks.add(tasks.value..add(task));
await downloadQueue.add(() => task.execute(account.client));
tasks.add(tasks.value..remove(task));

return future;
}

@override
Expand Down
4 changes: 2 additions & 2 deletions packages/neon/neon_files/lib/src/models/file_details.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ class FileDetails {
FileDetails.fromUploadTask({
required FilesUploadTask this.task,
}) : uri = task.uri,
size = task.stat.size,
lastModified = task.stat.modified,
size = task.size,
lastModified = task.lastModified,
etag = null,
mimeType = null,
hasPreview = null,
Expand Down
116 changes: 100 additions & 16 deletions packages/neon/neon_files/lib/src/utils/task.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:typed_data';

import 'package:meta/meta.dart';
import 'package:nextcloud/nextcloud.dart';
Expand All @@ -7,53 +8,136 @@ import 'package:universal_io/io.dart';
sealed class FilesTask {
FilesTask({
required this.uri,
required this.file,
});

final PathUri uri;

final File file;

@protected
final streamController = StreamController<double>();
final progressController = StreamController<double>();

/// Task progress in percent `[0, 1]`.
late final progress = streamController.stream.asBroadcastStream();
late final progress = progressController.stream.asBroadcastStream();
}

class FilesDownloadTask extends FilesTask {
sealed class FilesDownloadTask extends FilesTask {
FilesDownloadTask({
required super.uri,
});
}

sealed class FilesUploadTask extends FilesTask {
FilesUploadTask({
required super.uri,
required this.size,
required this.lastModified,
});

final int? size;

final DateTime? lastModified;
}

sealed class FilesTaskIO extends FilesTask {
FilesTaskIO({
required this.file,
required super.uri,
});

final File file;
}

sealed class FilesTaskMemory extends FilesTask {
FilesTaskMemory({
required super.uri,
});

final _stream = StreamController<List<int>>();

Stream<List<int>> get stream => _stream.stream;

void add(Uint8List chunk) => _stream.add(chunk);
}

class FilesDownloadTaskIO extends FilesTaskIO implements FilesDownloadTask {
FilesDownloadTaskIO({
required super.uri,
required super.file,
});

Future<void> execute(NextcloudClient client) async {
await client.webdav.getFile(
uri,
file,
onProgress: streamController.add,
onProgress: progressController.add,
);
await streamController.close();
await progressController.close();
}
}

class FilesUploadTask extends FilesTask {
FilesUploadTask({
class FilesUploadTaskIO extends FilesTaskIO implements FilesUploadTask {
FilesUploadTaskIO({
required super.uri,
required super.file,
});

FileStat? _stat;
FileStat get stat => _stat ??= file.statSync();
late final FileStat _stat = file.statSync();

@override
late int? size = _stat.size;

@override
late DateTime? lastModified = _stat.modified;

Future<void> execute(NextcloudClient client) async {
await client.webdav.putFile(
file,
stat,
_stat,
uri,
lastModified: _stat.modified,
onProgress: progressController.add,
);
await progressController.close();
}
}

class FilesDownloadTaskMemory extends FilesTaskMemory implements FilesDownloadTask {
FilesDownloadTaskMemory({
required super.uri,
});

Future<void> execute(NextcloudClient client) async {
final stream = client.webdav.getStream(
uri,
onProgress: progressController.add,
);
await stream.pipe(_stream);
await progressController.close();
}
}

class FilesUploadTaskMemory extends FilesTaskMemory implements FilesUploadTask {
FilesUploadTaskMemory({
required super.uri,
required this.size,
required this.lastModified,
required List<int> bytes,
}) {
unawaited(Stream.value(bytes).pipe(_stream));
}

@override
final int? size;

@override
final DateTime? lastModified;

Future<void> execute(NextcloudClient client) async {
await client.webdav.putStream(
_stream.stream,
uri,
lastModified: stat.modified,
onProgress: streamController.add,
lastModified: lastModified,
onProgress: progressController.add,
);
await streamController.close();
await progressController.close();
}
}
2 changes: 1 addition & 1 deletion packages/neon/neon_files/lib/src/widgets/actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class FileActions extends StatelessWidget {
final bloc = NeonProvider.of<FilesBloc>(context);
switch (action) {
case FilesFileAction.share:
bloc.shareFileNative(details.uri, details.etag!);
bloc.shareFileNative(details.uri, details.etag!, details.mimeType);
case FilesFileAction.toggleFavorite:
if (details.isFavorite ?? false) {
bloc.removeFavorite(details.uri);
Expand Down
Loading

0 comments on commit 275555c

Please sign in to comment.