[client] copy conflict handling

This commit is contained in:
Abhishek Shroff
2025-01-09 17:02:14 +05:30
parent 1ef4472755
commit 13d9d8ee2b
7 changed files with 112 additions and 58 deletions

View File

@@ -25,6 +25,7 @@ class ResourceCopyAction extends ResourceCreateAction with JsonApiAction {
final int contentLength;
final String contentSha256;
final DateTime timestamp;
final String? deletedId;
ResourceCopyAction({
required this.srcId,
@@ -39,6 +40,7 @@ class ResourceCopyAction extends ResourceCreateAction with JsonApiAction {
required this.contentSha256,
required this.description,
required this.timestamp,
required this.deletedId,
});
factory ResourceCopyAction.fromMap(Map<String, dynamic> map) {
@@ -55,6 +57,7 @@ class ResourceCopyAction extends ResourceCreateAction with JsonApiAction {
contentSha256: map['contentSha256'],
description: map['description'],
timestamp: DateTime.fromMillisecondsSinceEpoch(map['timestamp']),
deletedId: map['deletedId'],
);
}
@@ -72,6 +75,7 @@ class ResourceCopyAction extends ResourceCreateAction with JsonApiAction {
'contentSha256': contentSha256,
'description': description,
'timestamp': timestamp.millisecondsSinceEpoch,
'deletedId': deletedId,
};
@override
@@ -101,11 +105,18 @@ class ResourceCopyAction extends ResourceCreateAction with JsonApiAction {
),
);
}
if (deletedId != null) {
await account.db.updateResource(deletedId!, (u) => u(parent: Value(null)));
}
}
@override
Future<void> revertOptimisticUpdate() {
return account.db.deleteResource(resourceId);
Future<void> revertOptimisticUpdate() async {
await account.db.deleteResource(resourceId);
if (deletedId != null) {
// TODO: This should be reverted based on server data
await account.db.updateResource(deletedId!, (u) => u(parent: Value(parent)));
}
}
@override

View File

@@ -43,41 +43,6 @@ class ResourceRepository {
});
}
Future<String> copy({
required Resource resource,
required String destination,
}) async {
String name = resource.name;
if (resource.parent == destination) {
name = '${basenameWithoutExtension(name)} - copy${extension(name)}';
}
int counter = 0;
String localName = name;
var existing = await account.db.getResourcesByName(parent: destination, name: name);
while (existing.isNotEmpty) {
counter++;
localName = '${basenameWithoutExtension(name)} ${counter++}${extension(name)}';
existing = await account.db.getResourcesByName(parent: destination, name: localName);
}
String id = generateUuid();
await account.addAction(ResourceCopyAction(
srcId: resource.id,
resourceId: id,
parent: destination,
resourceName: name,
conflictResolution: nameConflictRename,
dir: resource.dir,
localName: localName,
contentType: resource.contentType,
contentLength: resource.contentLength,
contentSha256: resource.contentSha256,
description: 'Copying ${resource.name}',
timestamp: DateTime.now(),
));
return id;
}
Future<String> mkdir({
required String parent,
required String name,
@@ -123,6 +88,52 @@ class ResourceRepository {
return id;
}
Future<String> upload({
required String parent,
required String path,
String? name,
NameConflictResolution conflictResolution = nameConflictError,
}) async {
name ??= basename(path);
var existing = await account.db.getResourcesByName(parent: parent, name: name);
String localName = name;
String id = generateUuid();
if (existing.isNotEmpty) {
switch (conflictResolution) {
case nameConflictError:
throw const NameConflictException();
case nameConflictRename:
int counter = 0;
while (existing.isNotEmpty) {
counter++;
localName = '${basenameWithoutExtension(name)} ${counter++}${extension(name)}';
existing = await account.db.getResourcesByName(parent: parent, name: localName);
}
break;
case nameConflictOverwrite:
id = existing.first.id;
// TODO: Fix if existing resource is directory
default:
throw UnsupportedError('Unsupported conflict resolution');
}
}
final f = File(path);
final size = await f.length();
final contentType = lookupMimeType(path) ?? 'application/octet-stream';
await account.addAction(ResourceUploadAction(
resourceId: id,
parent: parent,
resourceName: name,
conflictResolution: conflictResolution,
timestamp: DateTime.now(),
path: path,
size: size,
contentType: contentType,
localName: localName,
));
return id;
}
Future<String> move({
required Resource resource,
required NameConflictResolution conflictResolution,
@@ -169,16 +180,21 @@ class ResourceRepository {
return resource.id;
}
Future<String> upload({
Future<String> copy({
required Resource resource,
required String parent,
required String path,
required NameConflictResolution conflictResolution,
String? name,
NameConflictResolution conflictResolution = nameConflictError,
}) async {
name ??= basename(path);
var existing = await account.db.getResourcesByName(parent: parent, name: name);
String localName = name;
name ??= resource.name;
if (parent == resource.parent && name == resource.name) {
name = '${basenameWithoutExtension(name)} - copy${extension(name)}';
}
String id = generateUuid();
var existing = await account.db.getResourcesByName(parent: parent, name: name);
String? deletedId;
String localName = name;
if (existing.isNotEmpty) {
switch (conflictResolution) {
case nameConflictError:
@@ -192,24 +208,28 @@ class ResourceRepository {
}
break;
case nameConflictOverwrite:
id = existing.first.id;
case nameConflictDelete:
conflictResolution = nameConflictDelete;
deletedId = existing.first.id;
break;
default:
throw UnsupportedError('Unsupported conflict resolution');
}
}
final f = File(path);
final size = await f.length();
final contentType = lookupMimeType(path) ?? 'application/octet-stream';
await account.addAction(ResourceUploadAction(
await account.addAction(ResourceCopyAction(
srcId: resource.id,
resourceId: id,
parent: parent,
resourceName: name,
conflictResolution: conflictResolution,
timestamp: DateTime.now(),
path: path,
size: size,
contentType: contentType,
conflictResolution: nameConflictRename,
dir: resource.dir,
localName: localName,
contentType: resource.contentType,
contentLength: resource.contentLength,
contentSha256: resource.contentSha256,
description: 'Copying ${resource.name}',
timestamp: DateTime.now(),
deletedId: deletedId,
));
return id;
}

View File

@@ -58,6 +58,7 @@ class ExplorerActions extends StatelessWidget {
handleNameConflict(
context,
name,
deleteButtonText: 'Overwrite',
(name, conflictResolution) => account.resourceRepository.move(
resource: r,
name: name,

View File

@@ -60,6 +60,7 @@ void handlePasteAction(PasteFromClipboardIntent i, BuildContext context) async {
handleNameConflict(
context,
r.name,
deleteButtonText: 'Overwrite',
(name, conflictResolution) => account.resourceRepository.move(
resource: r,
name: name,
@@ -69,7 +70,17 @@ void handlePasteAction(PasteFromClipboardIntent i, BuildContext context) async {
);
}
for (final r in copyResources) {
account.resourceRepository.copy(resource: r, destination: folderId);
handleNameConflict(
context,
r.name,
deleteButtonText: 'Overwrite',
(name, conflictResolution) => account.resourceRepository.copy(
resource: r,
name: name,
parent: folderId,
conflictResolution: conflictResolution,
),
);
}
await SystemClipboard.instance?.write([]);

View File

@@ -55,6 +55,7 @@ class _ResourceDragTargetState extends State<ResourceDragTarget> {
handleNameConflict(
context,
r.name,
deleteButtonText: 'Overwrite',
(name, conflictResolution) => account.resourceRepository.move(
resource: r,
name: name,

View File

@@ -50,6 +50,7 @@ void handleOption(BuildContext context, Iterable<Resource> resources, MenuOption
await handleNameConflict(
context,
r.name,
deleteButtonText: 'Overwrite',
(name, conflictResolution) => account.resourceRepository.move(
resource: r,
name: name,
@@ -70,6 +71,7 @@ void handleOption(BuildContext context, Iterable<Resource> resources, MenuOption
await handleNameConflict(
context,
r.name,
deleteButtonText: 'Overwrite',
(name, conflictResolution) => account.resourceRepository.move(
resource: r,
name: name,
@@ -86,10 +88,18 @@ void handleOption(BuildContext context, Iterable<Resource> resources, MenuOption
if (dest == null) {
return;
}
if (!context.mounted) return;
for (final r in resources) {
account.resourceRepository.copy(
resource: r,
destination: dest,
await handleNameConflict(
context,
r.name,
deleteButtonText: 'Overwrite',
(name, conflictResolution) => account.resourceRepository.copy(
resource: r,
name: name,
parent: dest,
conflictResolution: conflictResolution,
),
);
}
break;

View File

@@ -28,7 +28,7 @@ Future<void> createDirectory(BuildContext context, String folderId, {String? pre
validate: validateName,
);
if (name == null || !context.mounted) return;
handleNameConflict(
await handleNameConflict(
context,
name,
deleteButtonText: 'Overwrite',