Skip to content

Commit 413c01d

Browse files
committed
Improved size calculation when recycling files in browser control
1 parent 2c067aa commit 413c01d

1 file changed

Lines changed: 61 additions & 30 deletions

File tree

src/Sdk/SecureFolderFS.Sdk/ViewModels/Controls/Storage/Browser/BrowserItemViewModel.cs

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,12 @@ protected virtual async Task MoveAsync(CancellationToken cancellationToken)
117117
if (destinationViewModel is null || destinationViewModel.Inner.Id != destination.Id)
118118
return;
119119

120-
if (items.Any(item => destination.Id.Contains(item.Inner.Id, StringComparison.InvariantCultureIgnoreCase)))
120+
if (items.Any(item => IsAncestorOrSelf(destination.Id, item.Inner.Id)))
121121
return;
122122

123-
// Ensure the destination has content already loaded
123+
// Ensure the destination has content already loaded before collision checks
124124
if (destinationViewModel.Items.IsEmpty())
125-
_ = destinationViewModel.ListContentsAsync(cts.Token);
125+
await destinationViewModel.ListContentsAsync(cts.Token);
126126

127127
await transferViewModel.TransferAsync(items.Select(x => (IStorableChild)x.Inner), async (item, reporter, token) =>
128128
{
@@ -190,12 +190,12 @@ protected virtual async Task CopyAsync(CancellationToken cancellationToken)
190190
if (destinationViewModel is null || destinationViewModel.Inner.Id != destination.Id)
191191
return;
192192

193-
if (items.Any(item => destination.Id.Contains(item.Inner.Id, StringComparison.InvariantCultureIgnoreCase)))
193+
if (items.Any(item => IsAncestorOrSelf(destination.Id, item.Inner.Id)))
194194
return;
195195

196-
// Ensure the destination has content already loaded
196+
// Ensure the destination has content already loaded before collision checks
197197
if (destinationViewModel.Items.IsEmpty())
198-
_ = destinationViewModel.ListContentsAsync(cts.Token);
198+
await destinationViewModel.ListContentsAsync(cts.Token);
199199

200200
await transferViewModel.TransferAsync(items.Select(x => x.Inner), async (item, reporter, token) =>
201201
{
@@ -205,10 +205,6 @@ await transferViewModel.TransferAsync(items.Select(x => x.Inner), async (item, r
205205
// Copy
206206
var copiedItem = await modifiableDestination.CreateCopyOfStorableAsync(item, false, availableName, reporter, token);
207207

208-
// Ensure the destination has content already loaded
209-
if (destinationViewModel.Items.IsEmpty())
210-
_ = destinationViewModel.ListContentsAsync(cts.Token);
211-
212208
// Add to destination
213209
destinationViewModel.Items.Insert(copiedItem switch
214210
{
@@ -312,23 +308,24 @@ protected virtual async Task DeleteAsync(CancellationToken cancellationToken)
312308
if (recycleBin is null)
313309
return;
314310

315-
var sizes = new List<long>();
311+
// Key the sizes by item ID - the transfer callback receives the storable,
312+
// so positional lookups against the view model array would not match
313+
var sizes = new Dictionary<string, long>();
316314
foreach (var item in items)
317315
{
318-
sizes.Add(item.Inner switch
316+
sizes[item.Inner.Id] = item.Inner switch
319317
{
320318
IFile file => await file.GetSizeAsync(cts.Token) ?? 0L,
321319
IFolder folder => await folder.GetSizeAsync(cts.Token) ?? 0L,
322320
_ => 0L
323-
});
321+
};
324322
}
325323

326324
if (BrowserViewModel.Options.IsRecycleBinUnlimited())
327325
{
328326
await transferViewModel.TransferAsync(items.Select(x => (IStorableChild)x.Inner), async (item, token) =>
329327
{
330-
var idx = Array.IndexOf(items, item);
331-
await recyclableFolder.DeleteAsync(item, sizes[idx], false, token);
328+
await recyclableFolder.DeleteAsync(item, sizes.GetValueOrDefault(item.Id, 0L), false, token);
332329

333330
var itemToRemove = ParentFolder.Items.FirstOrDefault(x => x.Inner.Id == item.Id);
334331
if (itemToRemove is not null)
@@ -339,7 +336,7 @@ await transferViewModel.TransferAsync(items.Select(x => (IStorableChild)x.Inner)
339336
{
340337
var occupiedSize = await recycleBin.GetSizeAsync(cts.Token);
341338
var availableSize = BrowserViewModel.Options.RecycleBinSize - occupiedSize;
342-
var permanently = availableSize < sizes.Sum();
339+
var permanently = availableSize < sizes.Values.Sum();
343340

344341
if (permanently)
345342
{
@@ -356,16 +353,13 @@ await transferViewModel.TransferAsync(items.Select(x => (IStorableChild)x.Inner)
356353
return;
357354
}
358355

359-
var idx = 0;
360356
await transferViewModel.TransferAsync(items.Select(x => (IStorableChild)x.Inner), async (item, token) =>
361357
{
362-
await recyclableFolder.DeleteAsync(item, sizes[idx], permanently, token);
358+
await recyclableFolder.DeleteAsync(item, sizes.GetValueOrDefault(item.Id, 0L), permanently, token);
363359

364360
var itemToRemove = ParentFolder.Items.FirstOrDefault(x => x.Inner.Id == item.Id);
365361
if (itemToRemove is not null)
366362
ParentFolder.Items.RemoveAndGet(itemToRemove)?.Dispose();
367-
368-
idx++;
369363
}, cts.Token);
370364
}
371365
}
@@ -410,7 +404,7 @@ await transferViewModel.TransferAsync(items.Select(x => (IStorableChild)x.Inner)
410404
[RelayCommand]
411405
protected virtual async Task ExportAsync(CancellationToken cancellationToken)
412406
{
413-
if (ParentFolder?.Folder is not IModifiableFolder parentModifiableFolder)
407+
if (ParentFolder is null)
414408
return;
415409

416410
if (BrowserViewModel.TransferViewModel is not { IsProgressing: false } transferViewModel)
@@ -424,19 +418,56 @@ protected virtual async Task ExportAsync(CancellationToken cancellationToken)
424418
if (destination is not IModifiableFolder destinationFolder)
425419
return;
426420

427-
transferViewModel.TransferType = TransferType.Move;
428-
using var cts = transferViewModel.GetCancellation(cancellationToken);
429-
await transferViewModel.TransferAsync(items.Select(x => x.Inner), async (item, reporter, token) =>
430-
{
431-
// Copy and delete
432-
await destinationFolder.CreateCopyOfStorableAsync(item, false, reporter, token);
433-
await parentModifiableFolder.DeleteAsync((IStorableChild)item, token);
421+
// Sources in read-only vaults cannot be removed - export becomes a copy
422+
var removeSource = !BrowserViewModel.Options.IsReadOnly && ParentFolder.Folder is IModifiableFolder;
434423

435-
ParentFolder.Items.RemoveMatch(x => x.Inner.Id == item.Id)?.Dispose();
436-
}, cts.Token);
424+
try
425+
{
426+
transferViewModel.TransferType = removeSource ? TransferType.Move : TransferType.Copy;
427+
using var cts = transferViewModel.GetCancellation(cancellationToken);
428+
await transferViewModel.TransferAsync(items.Select(x => x.Inner), async (item, reporter, token) =>
429+
{
430+
// Copy and delete
431+
await destinationFolder.CreateCopyOfStorableAsync(item, false, reporter, token);
432+
if (removeSource && ParentFolder.Folder is IModifiableFolder parentModifiableFolder)
433+
{
434+
await parentModifiableFolder.DeleteAsync((IStorableChild)item, token);
435+
ParentFolder.Items.RemoveMatch(x => x.Inner.Id == item.Id)?.Dispose();
436+
}
437+
}, cts.Token);
438+
}
439+
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
440+
{
441+
_ = ex;
442+
// TODO: Report error
443+
}
444+
finally
445+
{
446+
await transferViewModel.HideAsync();
447+
}
437448
}
438449

439450
[RelayCommand]
440451
protected abstract Task OpenAsync(CancellationToken cancellationToken);
452+
453+
/// <summary>
454+
/// Determines whether <paramref name="destinationId"/> points to <paramref name="itemId"/> itself
455+
/// or to a descendant of it, respecting path segment boundaries.
456+
/// </summary>
457+
/// <remarks>
458+
/// A plain substring check would misclassify sibling paths that share a prefix (e.g. '/a/bc' and '/a/b').
459+
/// </remarks>
460+
private static bool IsAncestorOrSelf(string destinationId, string itemId)
461+
{
462+
if (destinationId.Equals(itemId, StringComparison.OrdinalIgnoreCase))
463+
return true;
464+
465+
if (!destinationId.StartsWith(itemId, StringComparison.OrdinalIgnoreCase))
466+
return false;
467+
468+
// The prefix must end exactly at a path separator boundary
469+
return itemId.EndsWith('/') || itemId.EndsWith('\\')
470+
|| destinationId[itemId.Length] is '/' or '\\';
471+
}
441472
}
442473
}

0 commit comments

Comments
 (0)