mirror of
https://codeberg.org/shroff/phylum.git
synced 2026-01-13 23:49:43 -06:00
362 lines
11 KiB
Dart
362 lines
11 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:file_selector/file_selector.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:phylum/libphylum/db/db.dart';
|
|
import 'package:phylum/libphylum/db/resource_helpers.dart';
|
|
import 'package:phylum/libphylum/local_upload_errors.dart';
|
|
import 'package:phylum/libphylum/phylum_account.dart';
|
|
import 'package:phylum/util/dialogs.dart';
|
|
import 'package:phylum/util/file_size.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
final slashChar = '/'.runes.first;
|
|
final illegalChars = <int>{
|
|
...'\\/:*?"<>|'.runes,
|
|
};
|
|
|
|
bool validateName(String name) =>
|
|
!name.startsWith(' ') &&
|
|
name != '.' &&
|
|
name != '..' &&
|
|
name.trim().isNotEmpty &&
|
|
!name.runes.any((c) => c < 31 || illegalChars.contains(c));
|
|
|
|
Future<void> createDirectory(BuildContext context, Resource parent, {String? preset}) async {
|
|
final account = context.read<PhylumAccount>();
|
|
final name = await showInputDialog(
|
|
context,
|
|
title: 'Create New Folder',
|
|
labelText: 'Folder Name',
|
|
preset: preset,
|
|
validate: validateName,
|
|
);
|
|
if (name == null || !context.mounted) return;
|
|
await handleLocalErrors(
|
|
context,
|
|
name,
|
|
overwriteFn: (r) => nameConflictDelete,
|
|
(name, conflictResolution, ensurePermission) => account.resourceRepository.mkdir(
|
|
parent: parent.id,
|
|
name: name,
|
|
userPermission: parent.userPermission,
|
|
conflictResolution: conflictResolution,
|
|
ensurePermission: ensurePermission,
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> pickAndUploadFiles(BuildContext context, String folderId) async {
|
|
final files = await openFiles();
|
|
if (!context.mounted) return;
|
|
return uploadXFiles(context, folderId, files);
|
|
}
|
|
|
|
Future<void> uploadXFiles(BuildContext context, String folderId, Iterable<XFile> files) async {
|
|
if (files.isEmpty) return;
|
|
final account = context.read<PhylumAccount>();
|
|
final size =
|
|
await Future.wait(files.map((files) => files.length())).then((sizes) => sizes.fold(0, (acc, s) => acc + s));
|
|
if (!context.mounted) return;
|
|
final confirm = await showAlertDialog(
|
|
context,
|
|
title: 'Upload ${files.length} Files?',
|
|
message: 'Total Upload Size: ${size.formatForDisplay()}',
|
|
positiveText: 'YES',
|
|
negativeText: 'NO',
|
|
) ??
|
|
false;
|
|
final userPermission = await account.db.getResource(folderId).then((r) => r?.userPermission) ?? 0;
|
|
if (!confirm || !context.mounted) return;
|
|
for (final file in files) {
|
|
String? id;
|
|
ensurefile:
|
|
while (id == null) {
|
|
id = await handleLocalErrors(
|
|
// ignore: use_build_context_synchronously
|
|
context,
|
|
p.basename(file.name),
|
|
overwriteFn: (r) => r.dir ? nameConflictDelete : nameConflictOverwrite,
|
|
(name, conflictResolution, ensurePermission) => account.resourceRepository.upload(
|
|
parent: folderId,
|
|
name: name,
|
|
file: file,
|
|
userPermission: userPermission,
|
|
conflictResolution: conflictResolution,
|
|
ensurePermission: ensurePermission,
|
|
),
|
|
);
|
|
if (id == null && context.mounted) {
|
|
final confirm =
|
|
await showAlertDialog(context, title: 'Skip file?', positiveText: 'No', negativeText: 'Yes') ?? false;
|
|
if (!confirm) break ensurefile;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> pickAndUploadDirectory(BuildContext context, String folderId) async {
|
|
final path = await getDirectoryPath();
|
|
if (path == null || !context.mounted) return;
|
|
return uploadPath(context, folderId, [path]);
|
|
}
|
|
|
|
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) {
|
|
final stat = await DirTree.stat(path);
|
|
tree = tree?.mergeWith(stat) ?? stat;
|
|
}
|
|
|
|
if (!context.mounted) return;
|
|
Navigator.of(context).pop();
|
|
|
|
if (tree == null) return;
|
|
final confirm = await showAlertDialog(
|
|
context,
|
|
title: 'Upload Files?',
|
|
message:
|
|
'Found ${tree.files.length} files and ${tree.dirs.length} directories.\n\nTotal Upload Size: ${tree.totalSize.formatForDisplay()}',
|
|
positiveText: 'YES',
|
|
negativeText: 'NO',
|
|
) ??
|
|
false;
|
|
final userPermission = await account.db.getResource(folderId).then((r) => r?.userPermission) ?? 0;
|
|
if (!confirm || !context.mounted) return;
|
|
final dirIds = <String, String>{};
|
|
for (final path in tree.dirs) {
|
|
final parent = dirIds[p.dirname(path)] ?? folderId;
|
|
String? id;
|
|
while (id == null) {
|
|
id = await handleLocalErrors(
|
|
// ignore: use_build_context_synchronously
|
|
context,
|
|
p.basename(path),
|
|
overwriteFn: (r) => nameConflictDelete,
|
|
(name, conflictResolution, ensurePermission) => account.resourceRepository.mkdir(
|
|
parent: parent,
|
|
name: name,
|
|
userPermission: userPermission,
|
|
conflictResolution: conflictResolution,
|
|
ensurePermission: ensurePermission,
|
|
),
|
|
);
|
|
if (id == null) {
|
|
if (!context.mounted) return;
|
|
final confirm =
|
|
await showAlertDialog(context, title: 'Cancel operation?', positiveText: 'No', negativeText: 'Yes') ??
|
|
false;
|
|
if (!confirm) return;
|
|
}
|
|
}
|
|
dirIds[path] = id;
|
|
}
|
|
|
|
for (final path in tree.files) {
|
|
final parent = dirIds[p.dirname(path)] ?? folderId;
|
|
String? id;
|
|
ensurefile:
|
|
while (id == null) {
|
|
id = await handleLocalErrors(
|
|
// ignore: use_build_context_synchronously
|
|
context,
|
|
p.basename(path),
|
|
overwriteFn: (r) => r.dir ? nameConflictDelete : nameConflictOverwrite,
|
|
(name, conflictResolution, ensurePermission) => account.resourceRepository.upload(
|
|
parent: parent,
|
|
name: name,
|
|
path: path,
|
|
userPermission: userPermission,
|
|
conflictResolution: conflictResolution,
|
|
ensurePermission: ensurePermission,
|
|
),
|
|
);
|
|
if (id == null && context.mounted) {
|
|
final confirm =
|
|
await showAlertDialog(context, title: 'Skip file?', positiveText: 'No', negativeText: 'Yes') ?? false;
|
|
if (!confirm) break ensurefile;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<String?> handleLocalErrors(
|
|
BuildContext context,
|
|
String name,
|
|
Future<String> Function(String, NameConflictResolution, bool) performAction, {
|
|
NameConflictResolution? Function(Resource)? overwriteFn,
|
|
NameConflictResolution conflictResolution = nameConflictError,
|
|
bool ensurePermission = true,
|
|
}) async {
|
|
try {
|
|
return await performAction(name, conflictResolution, ensurePermission);
|
|
} on PermissionException {
|
|
if (!context.mounted) return null;
|
|
final skip = await showAlertDialog(
|
|
context,
|
|
title: 'Permissions Error',
|
|
message: 'You do not have permissions to write to this folder.',
|
|
positiveText: 'OK',
|
|
negativeText: 'Try Anyway',
|
|
) ??
|
|
true;
|
|
if (!skip && context.mounted) {
|
|
return handleLocalErrors(
|
|
context,
|
|
name,
|
|
performAction,
|
|
overwriteFn: overwriteFn,
|
|
conflictResolution: conflictResolution,
|
|
ensurePermission: false,
|
|
);
|
|
}
|
|
} on NameConflictException catch (e) {
|
|
if (!context.mounted) return null;
|
|
final overwrite = overwriteFn?.call(e.resource);
|
|
final newResolution = await showDialog<NameConflictResolution>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Name Conflict'),
|
|
content: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 360),
|
|
child: Text(
|
|
'There is already another item with the name \'$name\' in this folder.',
|
|
softWrap: true,
|
|
),
|
|
),
|
|
actions: <Widget>[
|
|
if (overwrite != null)
|
|
TextButton(
|
|
style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.error),
|
|
child: const Text('Overwrite'),
|
|
onPressed: () {
|
|
Navigator.of(context).pop(overwrite);
|
|
},
|
|
),
|
|
TextButton(
|
|
child: const Text('Change Name'),
|
|
onPressed: () {
|
|
Navigator.of(context).pop(nameConflictError);
|
|
},
|
|
),
|
|
ElevatedButton(
|
|
autofocus: true,
|
|
child: const Text('Auto Rename'),
|
|
onPressed: () {
|
|
Navigator.of(context).pop(nameConflictRename);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
if (newResolution == null) {
|
|
return null;
|
|
}
|
|
if (newResolution == nameConflictError) {
|
|
if (!context.mounted) return null;
|
|
final newName = await showInputDialog(
|
|
context,
|
|
title: 'Change Name',
|
|
labelText: 'New Name',
|
|
preset: name,
|
|
validate: validateName,
|
|
);
|
|
if (newName == null) {
|
|
return null;
|
|
}
|
|
name = newName;
|
|
}
|
|
if (!context.mounted) return null;
|
|
return handleLocalErrors(
|
|
context,
|
|
name,
|
|
performAction,
|
|
overwriteFn: overwriteFn,
|
|
conflictResolution: newResolution,
|
|
ensurePermission: ensurePermission,
|
|
);
|
|
} on DestinationCycleException catch (e) {
|
|
if (context.mounted) {
|
|
await showAlertDialog(context, message: 'Cannot move ${e.resource.name} into itself');
|
|
}
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
await showAlertDialog(context, title: 'Unable to create $name', message: e.toString());
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
class DirTree {
|
|
final int numFiles;
|
|
final int numDirs;
|
|
final int totalSize;
|
|
final List<String> files;
|
|
final List<String> dirs;
|
|
|
|
DirTree({
|
|
required this.numFiles,
|
|
required this.numDirs,
|
|
required this.totalSize,
|
|
required this.files,
|
|
required this.dirs,
|
|
});
|
|
|
|
DirTree mergeWith(DirTree other) {
|
|
return DirTree(
|
|
numFiles: numFiles + other.numFiles,
|
|
numDirs: numDirs + other.numDirs,
|
|
totalSize: totalSize + other.totalSize,
|
|
files: List.of(files)..addAll(other.files),
|
|
dirs: List.of(dirs)..addAll(other.dirs),
|
|
);
|
|
}
|
|
|
|
static Future<DirTree> stat(String path) {
|
|
final s = FileStat.statSync(path);
|
|
if (s.type == FileSystemEntityType.file) {
|
|
return Future.value(DirTree(
|
|
numFiles: 1,
|
|
numDirs: 0,
|
|
totalSize: s.size,
|
|
files: [path],
|
|
dirs: const [],
|
|
));
|
|
}
|
|
final dir = Directory(path);
|
|
final completer = Completer<DirTree>();
|
|
final files = <String>[];
|
|
final dirs = <String>[path];
|
|
int numFiles = 0;
|
|
int numDirs = 0;
|
|
int totalSize = 0;
|
|
|
|
dir.list(recursive: true).listen(
|
|
(file) {
|
|
if (file is File) {
|
|
numFiles++;
|
|
totalSize += file.lengthSync();
|
|
files.add(file.path);
|
|
}
|
|
if (file is Directory) {
|
|
numDirs++;
|
|
dirs.add(file.path);
|
|
}
|
|
},
|
|
onDone: () => completer.complete(DirTree(
|
|
numFiles: numFiles,
|
|
numDirs: numDirs,
|
|
totalSize: totalSize,
|
|
files: files,
|
|
dirs: dirs,
|
|
)),
|
|
);
|
|
return completer.future;
|
|
}
|
|
}
|