Skip to content

Commit

Permalink
feat(neon_files): Implement file syncing
Browse files Browse the repository at this point in the history
Signed-off-by: jld3103 <[email protected]>
  • Loading branch information
provokateurin committed Dec 27, 2023
1 parent f458d3c commit df28e74
Show file tree
Hide file tree
Showing 11 changed files with 492 additions and 67 deletions.
8 changes: 8 additions & 0 deletions packages/app/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1421,6 +1421,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
web:
dependency: transitive
description:
Expand Down
4 changes: 4 additions & 0 deletions packages/neon/neon_files/lib/src/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:neon_files/src/blocs/files.dart';
import 'package:neon_files/src/options.dart';
import 'package:neon_files/src/pages/main.dart';
import 'package:neon_files/src/routes.dart';
import 'package:neon_files/src/sync/implementation.dart';
import 'package:neon_framework/models.dart';
import 'package:nextcloud/nextcloud.dart';

Expand Down Expand Up @@ -32,6 +33,9 @@ class FilesApp extends AppImplementation<FilesBloc, FilesOptions> {
@override
final Widget page = const FilesMainPage();

@override
final FilesSync syncImplementation = const FilesSync();

@override
final RouteBase route = $filesAppRoute;
}
54 changes: 15 additions & 39 deletions packages/neon/neon_files/lib/src/blocs/files.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ 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:open_file/open_file.dart';
Expand All @@ -21,8 +20,6 @@ import 'package:universal_io/io.dart';
abstract interface class FilesBlocEvents {
void uploadFile(final PathUri uri, final String localPath);

void syncFile(final PathUri uri);

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

void shareFileNative(final PathUri uri, final String etag);
Expand Down Expand Up @@ -75,6 +72,21 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
@override
BehaviorSubject<List<FilesTask>> tasks = BehaviorSubject<List<FilesTask>>.seeded([]);

@override
Future<void> refresh() async {
await browser.refresh();
}

@override
void removeFavorite(final PathUri uri) {
wrapAction(
() async => account.client.webdav.proppatch(
uri,
set: WebDavProp(ocfavorite: 0),
),
);
}

@override
void addFavorite(final PathUri uri) {
wrapAction(
Expand Down Expand Up @@ -127,21 +139,6 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
);
}

@override
Future<void> refresh() async {
await browser.refresh();
}

@override
void removeFavorite(final PathUri uri) {
wrapAction(
() async => account.client.webdav.proppatch(
uri,
set: WebDavProp(ocfavorite: 0),
),
);
}

@override
void rename(final PathUri uri, final String name) {
wrapAction(
Expand All @@ -152,27 +149,6 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
);
}

@override
void syncFile(final PathUri uri) {
wrapAction(
() async {
final file = File(
p.joinAll([
await NeonPlatform.instance.userAccessibleAppDataPath,
account.humanReadableID,
'files',
...uri.pathSegments,
]),
);
if (!file.parent.existsSync()) {
file.parent.createSync(recursive: true);
}
await _downloadFile(uri, file);
},
disableTimeout: true,
);
}

@override
void uploadFile(final PathUri uri, final String localPath) {
wrapAction(
Expand Down
122 changes: 122 additions & 0 deletions packages/neon/neon_files/lib/src/sync/implementation.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:neon_files/src/blocs/files.dart';
import 'package:neon_files/src/dialogs/choose_folder.dart';
import 'package:neon_files/src/models/file_details.dart';
import 'package:neon_files/src/sync/mapping.dart';
import 'package:neon_files/src/sync/sources.dart';
import 'package:neon_files/src/widgets/file_tile.dart';
import 'package:neon_framework/blocs.dart';
import 'package:neon_framework/models.dart';
import 'package:neon_framework/sync.dart';
import 'package:neon_framework/utils.dart';
import 'package:nextcloud/ids.dart';
import 'package:nextcloud/webdav.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:universal_io/io.dart';

@immutable
class FilesSync implements SyncImplementation<FilesSyncMapping, WebDavFile, FileSystemEntity> {
const FilesSync();

@override
String get appId => AppIDs.files;

@override
Future<FilesSyncSources> getSources(final Account account, final FilesSyncMapping mapping) async {
// This shouldn't be necessary, but it sadly is because of https://github.com/flutter/flutter/issues/25659.
// Alternative would be to use https://pub.dev/packages/shared_storage,
// but to be efficient we'd need https://github.com/alexrintt/shared-storage/issues/91
// or copy the files to the app cache (which is also not optimal).
if (Platform.isAndroid && !await Permission.manageExternalStorage.request().isGranted) {
throw const MissingPermissionException(Permission.manageExternalStorage);
}
return FilesSyncSources(
account.client,
mapping.remotePath,
mapping.localPath,
);
}

@override
Map<String, dynamic> serializeMapping(final FilesSyncMapping mapping) => mapping.toJson();

@override
FilesSyncMapping deserializeMapping(final Map<String, dynamic> json) => FilesSyncMapping.fromJson(json);

@override
Future<FilesSyncMapping?> addMapping(final BuildContext context, final Account account) async {
final accountsBloc = NeonProvider.of<AccountsBloc>(context);
final appsBloc = accountsBloc.getAppsBlocFor(account);
final filesBloc = appsBloc.getAppBlocByID(AppIDs.files)! as FilesBloc;
final filesBrowserBloc = filesBloc.getNewFilesBrowserBloc();

final remotePath = await showDialog<PathUri>(
context: context,
builder: (final context) => FilesChooseFolderDialog(
bloc: filesBrowserBloc,
filesBloc: filesBloc,
originalPath: PathUri.cwd(),
),
);
filesBrowserBloc.dispose();
if (remotePath == null) {
return null;
}

final localPath = await FileUtils.pickDirectory();
if (localPath == null) {
return null;
}
if (!context.mounted) {
return null;
}

return FilesSyncMapping(
appId: AppIDs.files,
accountId: account.id,
remotePath: remotePath,
localPath: Directory(localPath),
journal: SyncJournal(),
);
}

@override
String getMappingDisplayTitle(final FilesSyncMapping mapping) {
final path = mapping.remotePath.toString();
return path.substring(0, path.length - 1);
}

@override
String getMappingDisplaySubtitle(final FilesSyncMapping mapping) => mapping.localPath.path;

@override
String getMappingId(final FilesSyncMapping mapping) =>
'${Uri.encodeComponent(mapping.remotePath.toString())}-${Uri.encodeComponent(mapping.localPath.path)}';

@override
Widget getConflictDetailsLocal(final BuildContext context, final FileSystemEntity object) {
final stat = object.statSync();
return FilesFileTile(
showFullPath: true,
filesBloc: NeonProvider.of<FilesBloc>(context),
details: FileDetails(
uri: PathUri.parse(object.path),
size: stat.size,
etag: '',
mimeType: '',
lastModified: stat.modified,
hasPreview: false,
isFavorite: false,
),
);
}

@override
Widget getConflictDetailsRemote(final BuildContext context, final WebDavFile object) => FilesFileTile(
showFullPath: true,
filesBloc: NeonProvider.of<FilesBloc>(context),
details: FileDetails.fromWebDav(
file: object,
),
);
}
69 changes: 69 additions & 0 deletions packages/neon/neon_files/lib/src/sync/mapping.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:neon_framework/sync.dart';
import 'package:nextcloud/webdav.dart' as webdav;
import 'package:nextcloud/webdav.dart';
import 'package:universal_io/io.dart';
import 'package:watcher/watcher.dart';

part 'mapping.g.dart';

@JsonSerializable()
class FilesSyncMapping implements SyncMapping<webdav.WebDavFile, FileSystemEntity> {
FilesSyncMapping({
required this.accountId,
required this.appId,
required this.journal,
required this.remotePath,
required this.localPath,
});

factory FilesSyncMapping.fromJson(final Map<String, dynamic> json) => _$FilesSyncMappingFromJson(json);
Map<String, dynamic> toJson() => _$FilesSyncMappingToJson(this);

@override
final String accountId;

@override
final String appId;

@override
final SyncJournal journal;

@JsonKey(
fromJson: PathUri.parse,
toJson: _pathUriToJson,
)
final PathUri remotePath;

static String _pathUriToJson(final PathUri uri) => uri.toString();

@JsonKey(
fromJson: _directoryFromJson,
toJson: _directoryToJson,
)
final Directory localPath;

static Directory _directoryFromJson(final String value) => Directory(value);
static String _directoryToJson(final Directory value) => value.path;

StreamSubscription<WatchEvent>? _subscription;

@override
void watch(final void Function() onUpdated) {
debugPrint('Watching file changes: $localPath');
_subscription ??= DirectoryWatcher(localPath.path).events.listen(
(final event) {
debugPrint('Registered file change: ${event.path} ${event.type}');
onUpdated();
},
);
}

@override
void dispose() {
unawaited(_subscription?.cancel());
}
}
23 changes: 23 additions & 0 deletions packages/neon/neon_files/lib/src/sync/mapping.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit df28e74

Please sign in to comment.