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 Aug 24, 2023
1 parent 52e5142 commit f9a4f41
Show file tree
Hide file tree
Showing 14 changed files with 492 additions and 101 deletions.
8 changes: 8 additions & 0 deletions packages/app/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1430,6 +1430,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
2 changes: 1 addition & 1 deletion packages/neon/neon/lib/src/blocs/sync.dart
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ class SyncBloc extends InteractiveBloc implements SyncBlocEvents, SyncBlocStates
mappingStatuses.add({
for (final mapping in loadedMappings) ...{
mapping: SyncMappingStatus.unknown,
}
},
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,11 @@ class AppImplementationSettingsPage extends StatelessWidget {
);
},
);
// ignore: use_build_context_synchronously
if (account == null || !context.mounted) {
if (account == null) {
return;
}

if (!context.mounted) {
return;
}

Expand Down
65 changes: 21 additions & 44 deletions packages/neon/neon_files/lib/blocs/files.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ part of '../neon_files.dart';
abstract class FilesBlocEvents {
void uploadFile(final List<String> path, final String localPath);

void syncFile(final List<String> path);

void openFile(final List<String> path, final String etag, final String? mimeType);

void delete(final List<String> path);
Expand All @@ -31,8 +29,8 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
this._requestManager,
this._platform,
) {
options.uploadQueueParallelism.addListener(_uploadParalelismListener);
options.downloadQueueParallelism.addListener(_downloadParalelismListener);
options.uploadQueueParallelism.addListener(_uploadParallelismListener);
options.downloadQueueParallelism.addListener(_downloadParallelismListener);
}

final FilesAppSpecificOptions options;
Expand All @@ -50,13 +48,28 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
_downloadQueue.dispose();
unawaited(tasks.close());

options.uploadQueueParallelism.removeListener(_uploadParalelismListener);
options.downloadQueueParallelism.removeListener(_downloadParalelismListener);
options.uploadQueueParallelism.removeListener(_uploadParallelismListener);
options.downloadQueueParallelism.removeListener(_downloadParallelismListener);
}

@override
BehaviorSubject<List<FilesTask>> tasks = BehaviorSubject<List<FilesTask>>.seeded([]);

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

@override
void removeFavorite(final List<String> path) {
wrapAction(
() async => account.client.webdav.proppatch(
path.join('/'),
set: WebDavProp(ocfavorite: 0),
),
);
}

@override
void addFavorite(final List<String> path) {
wrapAction(
Expand Down Expand Up @@ -110,21 +123,6 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
);
}

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

@override
void removeFavorite(final List<String> path) {
wrapAction(
() async => account.client.webdav.proppatch(
path.join('/'),
set: WebDavProp(ocfavorite: 0),
),
);
}

@override
void rename(final List<String> path, final String name) {
wrapAction(
Expand All @@ -135,27 +133,6 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
);
}

@override
void syncFile(final List<String> path) {
wrapAction(
() async {
final file = File(
p.join(
await _platform.getUserAccessibleAppDataPath(),
account.humanReadableID,
'files',
path.join(Platform.pathSeparator),
),
);
if (!file.parent.existsSync()) {
file.parent.createSync(recursive: true);
}
await _downloadFile(path, file);
},
disableTimeout: true,
);
}

@override
void uploadFile(final List<String> path, final String localPath) {
wrapAction(
Expand Down Expand Up @@ -187,11 +164,11 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta

FilesBrowserBloc getNewFilesBrowserBloc() => FilesBrowserBloc(_requestManager, options, account);

void _downloadParalelismListener() {
void _downloadParallelismListener() {
_downloadQueue.parallel = options.downloadQueueParallelism.value;
}

void _uploadParalelismListener() {
void _uploadParallelismListener() {
_uploadQueue.parallel = options.uploadQueueParallelism.value;
}
}
22 changes: 4 additions & 18 deletions packages/neon/neon_files/lib/models/file_details.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ class FileDetails {

FileDetails.fromWebDav({
required final WebDavFile file,
required final List<String> path,
}) : path = List.from(path)..add(file.name),
}) : path = file.path.split('/'),
isDirectory = file.isDirectory,
size = file.size,
etag = file.etag,
Expand All @@ -37,28 +36,15 @@ class FileDetails {
hasPreview = null,
isFavorite = null;

FileDetails.fromDownloadTask({
required FilesDownloadTask this.task,
required final WebDavFile file,
}) : path = task.path,
isDirectory = file.isDirectory,
size = file.size,
etag = file.etag,
mimeType = file.mimeType,
lastModified = file.lastModified,
hasPreview = file.hasPreview,
isFavorite = file.favorite;

factory FileDetails.fromTask({
required final FilesTask task,
required final FilesTask? task,
required final WebDavFile file,
}) {
switch (task) {
case FilesUploadTask():
return FileDetails.fromUploadTask(task: task);
case FilesDownloadTask():
return FileDetails.fromDownloadTask(
task: task,
default:
return FileDetails.fromWebDav(
file: file,
);
}
Expand Down
9 changes: 9 additions & 0 deletions packages/neon/neon_files/lib/neon_files.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ import 'package:neon/nextcloud.dart';
import 'package:neon/platform.dart';
import 'package:neon/settings.dart';
import 'package:neon/sort_box.dart';
import 'package:neon/sync.dart';
import 'package:neon/theme.dart';
import 'package:neon/utils.dart';
import 'package:neon/widgets.dart';
import 'package:neon_files/l10n/localizations.dart';
import 'package:neon_files/routes.dart';
import 'package:neon_files/sync/mapping.dart';
import 'package:neon_files/widgets/file_list_tile.dart';
import 'package:open_file/open_file.dart';
import 'package:path/path.dart' as p;
Expand All @@ -39,9 +42,12 @@ part 'options.dart';
part 'pages/details.dart';
part 'pages/main.dart';
part 'sort/files.dart';
part 'sync/implementation.dart';
part 'sync/sources.dart';
part 'utils/task.dart';
part 'widgets/browser_view.dart';
part 'widgets/file_preview.dart';
part 'widgets/file_tile.dart';

class FilesApp extends AppImplementation<FilesBloc, FilesAppSpecificOptions> {
FilesApp(super.sharedPreferences, super.requestManager, super.platform);
Expand All @@ -66,6 +72,9 @@ class FilesApp extends AppImplementation<FilesBloc, FilesAppSpecificOptions> {
platform,
);

@override
FilesSync getSync() => FilesSync();

@override
Widget get page => const FilesMainPage();

Expand Down
112 changes: 112 additions & 0 deletions packages/neon/neon_files/lib/sync/implementation.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
part of '../neon_files.dart';

class FilesSync implements SyncImplementation<FilesSyncMapping, WebDavFile, FileSystemEntity> {
@override
String appId = AppIDs.files;

@override
FilesSyncSources getSources(final Account account, final FilesSyncMapping mapping) => 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 = Provider.of<AccountsBloc>(context, listen: false);
final appsBloc = accountsBloc.getAppsBlocFor(account);
final filesBloc = appsBloc.getAppBlocByID(AppIDs.files)! as FilesBloc;
final filesBrowserBloc = filesBloc.getNewFilesBrowserBloc();

final remotePath = await showDialog<List<String>?>(
context: context,
builder: (final context) => FilesChooseFolderDialog(
bloc: filesBrowserBloc,
filesBloc: filesBloc,
originalPath: const [],
),
);
filesBrowserBloc.dispose();
if (remotePath == null) {
return null;
}

final localPath = await FileUtils.pickDirectory();
// ignore: use_build_context_synchronously
if (localPath == null || !context.mounted) {
return null;
}

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

@override
List<Widget> buildConflictTiles(
final BuildContext context,
final Function(SyncConflictSolution) onSolution,
final SyncConflictSolution? selectedSolution,
final SyncMapping<WebDavFile, FileSystemEntity> mapping,
final String id,
final FileSystemEntity localObject,
final WebDavFile remoteObject,
) {
final filesBloc = Provider.of<FilesBloc>(context, listen: false);
final stat = localObject.statSync();
mapping as FilesSyncMapping;
return [
FilesFileTile(
showFullPath: true,
filesBloc: filesBloc,
details: FileDetails(
path: '${mapping.localPath}/$id'.split('/'),
isDirectory: localObject is Directory,
size: stat.size,
etag: '',
mimeType: '',
lastModified: stat.modified,
hasPreview: false,
isFavorite: false,
),
onTap: () {
onSolution(SyncConflictSolution.overwriteA);
},
trailing: Radio<SyncConflictSolution>(
value: SyncConflictSolution.overwriteA,
groupValue: selectedSolution,
onChanged: (final solution) {
onSolution(solution!);
},
),
),
FilesFileTile(
showFullPath: true,
filesBloc: filesBloc,
details: FileDetails.fromWebDav(
file: remoteObject,
),
onTap: () {
onSolution(SyncConflictSolution.overwriteB);
},
trailing: Radio<SyncConflictSolution>(
value: SyncConflictSolution.overwriteB,
groupValue: selectedSolution,
onChanged: (final solution) {
onSolution(solution!);
},
),
),
];
}
}
52 changes: 52 additions & 0 deletions packages/neon/neon_files/lib/sync/mapping.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'dart:async';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:neon/nextcloud.dart';
import 'package:neon/sync.dart';
import 'package:watcher/watcher.dart';

part 'mapping.g.dart';

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

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

final List<String> remotePath;

final String localPath;

@override
late String title = remotePath.join('/');

@override
late String subtitle = localPath;

StreamSubscription<WatchEvent>? _subscription;

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

@override
void dispose() {
unawaited(_subscription?.cancel());
}
}
Loading

0 comments on commit f9a4f41

Please sign in to comment.