Skip to content

Commit 0efb5aa

Browse files
Feature: Show error when a shortcut can't be created (#11013)
1 parent ba8d70f commit 0efb5aa

File tree

7 files changed

+141
-91
lines changed

7 files changed

+141
-91
lines changed

src/Files.App/Dialogs/CreateShortcutDialog.xaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@
4949
Grid.Column="0"
5050
HorizontalAlignment="Stretch"
5151
PlaceholderText="C:\Users\"
52-
Text="{x:Bind ViewModel.DestinationItemPath, Mode=TwoWay}"
53-
TextChanged="DestinationItemPath_TextChanged" />
52+
Text="{x:Bind ViewModel.DestinationItemPath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
5453
<Button
5554
x:Name="SelectDestination"
5655
Grid.Row="2"

src/Files.App/Dialogs/CreateShortcutDialog.xaml.cs

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,32 +22,5 @@ public CreateShortcutDialog()
2222
}
2323

2424
public new async Task<DialogResult> ShowAsync() => (DialogResult)await base.ShowAsync();
25-
26-
private void DestinationItemPath_TextChanged(object sender, TextChangedEventArgs e)
27-
{
28-
if (string.IsNullOrWhiteSpace(DestinationItemPath.Text))
29-
{
30-
ViewModel.IsLocationValid = false;
31-
return;
32-
}
33-
34-
try
35-
{
36-
ViewModel.DestinationPathExists = Path.Exists(DestinationItemPath.Text) && DestinationItemPath.Text != Path.GetPathRoot(DestinationItemPath.Text);
37-
if (ViewModel.DestinationPathExists)
38-
{
39-
ViewModel.IsLocationValid = true;
40-
}
41-
else
42-
{
43-
var uri = new Uri(DestinationItemPath.Text);
44-
ViewModel.IsLocationValid = uri.IsWellFormedOriginalString();
45-
}
46-
}
47-
catch (Exception)
48-
{
49-
ViewModel.IsLocationValid = false;
50-
}
51-
}
5225
}
5326
}

src/Files.App/Filesystem/FilesystemOperations/ShellFilesystemOperations.cs

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -273,28 +273,33 @@ public async Task<IStorageHistory> CreateShortcutItemsAsync(IList<IStorageItemWi
273273

274274
FileSystemProgress fsProgress = new(progress, true, FileSystemStatusCode.InProgress, source.Count);
275275
fsProgress.Report();
276+
276277
var items = source.Zip(destination, (src, dest, index) => new { src, dest, index }).Where(x => !string.IsNullOrEmpty(x.src.Path) && !string.IsNullOrEmpty(x.dest));
277278
foreach (var item in items)
278279
{
279-
if (await FileOperationsHelpers.CreateOrUpdateLinkAsync(item.dest, item.src.Path))
280+
var result = await FileOperationsHelpers.CreateOrUpdateLinkAsync(item.dest, item.src.Path);
281+
282+
if (!result)
283+
result = await UIFilesystemHelpers.HandleShortcutCannotBeCreated(Path.GetFileName(item.dest), item.src.Path);
284+
285+
if (result)
280286
{
281287
createdSources.Add(item.src);
282288
createdDestination.Add(StorageHelpers.FromPathAndType(item.dest, FilesystemItemType.File));
283289
}
290+
284291
fsProgress.ProcessedItemsCount = item.index;
285292
fsProgress.Report();
286293
}
287294

288295
fsProgress.ReportStatus(createdSources.Count == source.Count ? FileSystemStatusCode.Success : FileSystemStatusCode.Generic);
296+
289297
return new StorageHistory(FileOperationType.CreateLink, createdSources, createdDestination);
290298
}
291299

292300
public async Task<IStorageHistory> DeleteAsync(IStorageItem source, IProgress<FileSystemProgress> progress, bool permanently, CancellationToken cancellationToken)
293301
{
294-
return await DeleteAsync(source.FromStorageItem(),
295-
progress,
296-
permanently,
297-
cancellationToken);
302+
return await DeleteAsync(source.FromStorageItem(), progress, permanently, cancellationToken);
298303
}
299304

300305
public async Task<IStorageHistory> DeleteAsync(IStorageItemWithPath source, IProgress<FileSystemProgress> progress, bool permanently, CancellationToken cancellationToken)
@@ -311,14 +316,16 @@ public async Task<IStorageHistory> DeleteItemsAsync(IList<IStorageItemWithPath>
311316
{
312317
if (source.Any(x => string.IsNullOrWhiteSpace(x.Path) || x.Path.StartsWith(@"\\?\", StringComparison.Ordinal) || FtpHelpers.IsFtpPath(x.Path)))
313318
{
314-
// Fallback to builtin file operations
319+
// Fallback to built-in file operations
315320
return await filesystemOperations.DeleteItemsAsync(source, progress, permanently, cancellationToken);
316321
}
317322

318323
FileSystemProgress fsProgress = new(progress, true, FileSystemStatusCode.InProgress);
319324
fsProgress.Report();
325+
320326
var deleleFilePaths = source.Select(s => s.Path).Distinct();
321327
var deleteFromRecycleBin = source.Any() && RecycleBinHelpers.IsPathUnderRecycleBin(source.ElementAt(0).Path);
328+
322329
permanently |= deleteFromRecycleBin;
323330

324331
if (deleteFromRecycleBin)
@@ -341,10 +348,12 @@ public async Task<IStorageHistory> DeleteItemsAsync(IList<IStorageItemWithPath>
341348
if (result)
342349
{
343350
fsProgress.ReportStatus(FileSystemStatusCode.Success);
351+
344352
foreach (var item in deleteResult.Items)
345353
{
346354
await associatedInstance.FilesystemViewModel.RemoveFileOrFolderAsync(item.Source);
347355
}
356+
348357
var recycledSources = deleteResult.Items.Where(x => x.Succeeded && x.Destination is not null && x.Source != x.Destination);
349358
if (recycledSources.Any())
350359
{
@@ -356,22 +365,22 @@ public async Task<IStorageHistory> DeleteItemsAsync(IList<IStorageItemWithPath>
356365
await recycledSources.Zip(sourceMatch, (rSrc, oSrc) => new { rSrc, oSrc })
357366
.Select(item => StorageHelpers.FromPathAndType(item.rSrc.Destination, item.oSrc.ItemType)).ToListAsync());
358367
}
368+
359369
return new StorageHistory(FileOperationType.Delete, source, null);
360370
}
361371
else
362372
{
363373
if (deleteResult.Items.Any(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.Unauthorized))
364374
{
365375
if (await RequestAdminOperation())
366-
{
367376
return await DeleteItemsAsync(source, progress, permanently, cancellationToken);
368-
}
369377
}
370378
else if (deleteResult.Items.Any(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.InUse))
371379
{
372380
var failedSources = deleteResult.Items.Where(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.InUse);
373381
var filePath = failedSources.Select(x => x.Source); // When deleting only source can be in use but shell returns COPYENGINE_E_SHARING_VIOLATION_DEST for folders
374382
var lockingProcess = WhoIsLocking(filePath);
383+
375384
switch (await GetFileInUseDialog(filePath, lockingProcess))
376385
{
377386
case DialogResult.Primary:
@@ -391,20 +400,18 @@ public async Task<IStorageHistory> DeleteItemsAsync(IList<IStorageItemWithPath>
391400
// Retry with StorageFile API
392401
var failedSources = deleteResult.Items.Where(x => !x.Succeeded);
393402
var sourceMatch = await failedSources.Select(x => source.DistinctBy(x => x.Path).SingleOrDefault(s => s.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync();
403+
394404
return await filesystemOperations.DeleteItemsAsync(sourceMatch, progress, permanently, cancellationToken);
395405
}
396406
fsProgress.ReportStatus(CopyEngineResult.Convert(deleteResult.Items.FirstOrDefault(x => !x.Succeeded)?.HResult));
407+
397408
return null;
398409
}
399410
}
400411

401412
public async Task<IStorageHistory> MoveAsync(IStorageItem source, string destination, NameCollisionOption collision, IProgress<FileSystemProgress> progress, CancellationToken cancellationToken)
402413
{
403-
return await MoveAsync(source.FromStorageItem(),
404-
destination,
405-
collision,
406-
progress,
407-
cancellationToken);
414+
return await MoveAsync(source.FromStorageItem(), destination, collision, progress, cancellationToken);
408415
}
409416

410417
public async Task<IStorageHistory> MoveAsync(IStorageItemWithPath source, string destination, NameCollisionOption collision, IProgress<FileSystemProgress> progress, CancellationToken cancellationToken)
@@ -427,20 +434,21 @@ public async Task<IStorageHistory> MoveItemsAsync(IList<IStorageItemWithPath> so
427434

428435
FileSystemProgress fsProgress = new(progress, true, FileSystemStatusCode.InProgress);
429436
fsProgress.Report();
437+
430438
var sourceNoSkip = source.Zip(collisions, (src, coll) => new { src, coll }).Where(item => item.coll != FileNameConflictResolveOptionType.Skip).Select(item => item.src);
431439
var destinationNoSkip = destination.Zip(collisions, (src, coll) => new { src, coll }).Where(item => item.coll != FileNameConflictResolveOptionType.Skip).Select(item => item.src);
432440
var collisionsNoSkip = collisions.Where(c => c != FileNameConflictResolveOptionType.Skip);
433-
434441
var operationID = Guid.NewGuid().ToString();
442+
435443
using var r = cancellationToken.Register(CancelOperation, operationID, false);
436444

437445
var sourceReplace = sourceNoSkip.Zip(collisionsNoSkip, (src, coll) => new { src, coll }).Where(item => item.coll == FileNameConflictResolveOptionType.ReplaceExisting).Select(item => item.src);
438446
var destinationReplace = destinationNoSkip.Zip(collisionsNoSkip, (src, coll) => new { src, coll }).Where(item => item.coll == FileNameConflictResolveOptionType.ReplaceExisting).Select(item => item.src);
439447
var sourceRename = sourceNoSkip.Zip(collisionsNoSkip, (src, coll) => new { src, coll }).Where(item => item.coll != FileNameConflictResolveOptionType.ReplaceExisting).Select(item => item.src);
440448
var destinationRename = destinationNoSkip.Zip(collisionsNoSkip, (src, coll) => new { src, coll }).Where(item => item.coll != FileNameConflictResolveOptionType.ReplaceExisting).Select(item => item.src);
441-
442449
var result = (FilesystemResult)true;
443450
var moveResult = new ShellOperationResult();
451+
444452
if (sourceRename.Any())
445453
{
446454
var (status, response) = await FileOperationsHelpers.MoveItemAsync(sourceRename.Select(s => s.Path).ToArray(), destinationRename.ToArray(), false, NativeWinApiHelper.CoreWindowHandle.ToInt64(), operationID, progress);
@@ -461,16 +469,19 @@ public async Task<IStorageHistory> MoveItemsAsync(IList<IStorageItemWithPath> so
461469
if (result)
462470
{
463471
fsProgress.ReportStatus(FileSystemStatusCode.Success);
472+
464473
var movedSources = moveResult.Items.Where(x => x.Succeeded && x.Destination is not null && x.Source != x.Destination);
465474
if (movedSources.Any())
466475
{
467476
var sourceMatch = await movedSources.Select(x => sourceRename
468477
.SingleOrDefault(s => s.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync();
478+
469479
return new StorageHistory(FileOperationType.Move,
470480
sourceMatch,
471481
await movedSources.Zip(sourceMatch, (rSrc, oSrc) => new { rSrc, oSrc })
472482
.Select(item => StorageHelpers.FromPathAndType(item.rSrc.Destination, item.oSrc.ItemType)).ToListAsync());
473483
}
484+
474485
return null; // Cannot undo overwrite operation
475486
}
476487
else
@@ -479,26 +490,27 @@ public async Task<IStorageHistory> MoveItemsAsync(IList<IStorageItemWithPath> so
479490
if (moveResult.Items.Any(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.Unauthorized))
480491
{
481492
if (await RequestAdminOperation())
482-
{
483493
return await MoveItemsAsync(source, destination, collisions, progress, cancellationToken);
484-
}
485494
}
486495
else if (source.Zip(destination, (src, dest) => (src, dest)).FirstOrDefault(x => x.src.ItemType == FilesystemItemType.Directory && PathNormalization.GetParentDir(x.dest).IsSubPathOf(x.src.Path)) is (IStorageItemWithPath, string) subtree)
487496
{
488497
var destName = subtree.dest.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries).Last();
489498
var srcName = subtree.src.Path.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries).Last();
499+
490500
await DialogDisplayHelper.ShowDialogAsync("ErrorDialogThisActionCannotBeDone".GetLocalizedResource(), $"{"ErrorDialogTheDestinationFolder".GetLocalizedResource()} ({destName}) {"ErrorDialogIsASubfolder".GetLocalizedResource()} ({srcName})");
491501
}
492502
else if (moveResult.Items.Any(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.InUse))
493503
{
494504
var failedSources = moveResult.Items.Where(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.InUse);
495505
var filePath = failedSources.Select(x => x.HResult == CopyEngineResult.COPYENGINE_E_SHARING_VIOLATION_SRC ? x.Source : x.Destination);
496506
var lockingProcess = WhoIsLocking(filePath);
507+
497508
switch (await GetFileInUseDialog(filePath, lockingProcess))
498509
{
499510
case DialogResult.Primary:
500511
var moveZip = sourceNoSkip.Zip(destinationNoSkip, (src, dest) => new { src, dest }).Zip(collisionsNoSkip, (z1, coll) => new { z1.src, z1.dest, coll });
501512
var sourceMatch = await failedSources.Select(x => moveZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync();
513+
502514
return await MoveItemsAsync(
503515
await sourceMatch.Select(x => x.src).ToListAsync(),
504516
await sourceMatch.Select(x => x.dest).ToListAsync(),
@@ -511,6 +523,7 @@ await sourceMatch.Select(x => x.dest).ToListAsync(),
511523
var failedSources = moveResult.Items.Where(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.NameTooLong);
512524
var moveZip = sourceNoSkip.Zip(destinationNoSkip, (src, dest) => new { src, dest }).Zip(collisionsNoSkip, (z1, coll) => new { z1.src, z1.dest, coll });
513525
var sourceMatch = await failedSources.Select(x => moveZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync();
526+
514527
return await filesystemOperations.MoveItemsAsync(
515528
await sourceMatch.Select(x => x.src).ToListAsync(),
516529
await sourceMatch.Select(x => x.dest).ToListAsync(),
@@ -530,11 +543,13 @@ await sourceMatch.Select(x => x.dest).ToListAsync(),
530543
var failedSources = moveResult.Items.Where(x => !x.Succeeded);
531544
var moveZip = sourceNoSkip.Zip(destinationNoSkip, (src, dest) => new { src, dest }).Zip(collisionsNoSkip, (z1, coll) => new { z1.src, z1.dest, coll });
532545
var sourceMatch = await failedSources.Select(x => moveZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync();
546+
533547
return await filesystemOperations.MoveItemsAsync(
534548
await sourceMatch.Select(x => x.src).ToListAsync(),
535549
await sourceMatch.Select(x => x.dest).ToListAsync(),
536550
await sourceMatch.Select(x => x.coll).ToListAsync(), progress, cancellationToken);
537551
}
552+
538553
return null;
539554
}
540555
}
@@ -554,41 +569,42 @@ public async Task<IStorageHistory> RenameAsync(IStorageItemWithPath source, stri
554569

555570
FileSystemProgress fsProgress = new(progress, true, FileSystemStatusCode.InProgress);
556571
fsProgress.Report();
557-
var renameResult = new ShellOperationResult();
558572

573+
var renameResult = new ShellOperationResult();
559574
var (status, response) = await FileOperationsHelpers.RenameItemAsync(source.Path, newName, collision == NameCollisionOption.ReplaceExisting);
560-
561575
var result = (FilesystemResult)status;
576+
562577
renameResult.Items.AddRange(response?.Final ?? Enumerable.Empty<ShellOperationItemResult>());
563578

564579
result &= (FilesystemResult)renameResult.Items.All(x => x.Succeeded);
565580

566581
if (result)
567582
{
568583
fsProgress.ReportStatus(FileSystemStatusCode.Success);
584+
569585
var renamedSources = renameResult.Items.Where(x => x.Succeeded && x.Destination is not null && x.Source != x.Destination)
570586
.Where(x => new[] { source }.Select(s => s.Path).Contains(x.Source));
571587
if (renamedSources.Any())
572588
{
573589
return new StorageHistory(FileOperationType.Rename, source,
574590
StorageHelpers.FromPathAndType(renamedSources.Single().Destination, source.ItemType));
575591
}
592+
576593
return null; // Cannot undo overwrite operation
577594
}
578595
else
579596
{
580597
if (renameResult.Items.Any(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.Unauthorized))
581598
{
582599
if (await RequestAdminOperation())
583-
{
584600
return await RenameAsync(source, newName, collision, progress, cancellationToken);
585-
}
586601
}
587602
else if (renameResult.Items.Any(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.InUse))
588603
{
589604
var failedSources = renameResult.Items.Where(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.InUse);
590605
var filePath = failedSources.Select(x => x.HResult == CopyEngineResult.COPYENGINE_E_SHARING_VIOLATION_SRC ? x.Source : x.Destination);
591606
var lockingProcess = WhoIsLocking(filePath);
607+
592608
switch (await GetFileInUseDialog(filePath, lockingProcess))
593609
{
594610
case DialogResult.Primary:
@@ -613,7 +629,9 @@ public async Task<IStorageHistory> RenameAsync(IStorageItemWithPath source, stri
613629
// Retry with StorageFile API
614630
return await filesystemOperations.RenameAsync(source, newName, collision, progress, cancellationToken);
615631
}
632+
616633
fsProgress.ReportStatus(CopyEngineResult.Convert(renameResult.Items.FirstOrDefault(x => !x.Succeeded)?.HResult));
634+
617635
return null;
618636
}
619637
}

0 commit comments

Comments
 (0)