Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Show error when a shortcut can't be created #11013

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a1cb1c7
Feature: "Can't create shortcut" modal
ferrariofilippo Jan 14, 2023
a479eaf
ShortcutCompleteName default value
ferrariofilippo Jan 14, 2023
e34555f
Resource value
ferrariofilippo Jan 15, 2023
7d0fc3a
Merge branch 'main' into Feature_Cannot_Create_Shortcut_Dialog
ferrariofilippo Jan 15, 2023
0968fad
Merge branch 'main' into Feature_Cannot_Create_Shortcut_Dialog
yaira2 Jan 15, 2023
ed5d4c0
Merge branch 'main' into Feature_Cannot_Create_Shortcut_Dialog
ferrariofilippo Jan 19, 2023
98a833a
Modal description & Bug fix
ferrariofilippo Jan 19, 2023
ac3a76a
Spacing
ferrariofilippo Jan 19, 2023
4df9c26
Added back binding
ferrariofilippo Jan 19, 2023
c5df826
Merge branch 'main' into Feature_Cannot_Create_Shortcut_Dialog
ferrariofilippo Jan 19, 2023
c4f23d6
Merge branch 'main' into Feature_Cannot_Create_Shortcut_Dialog
ferrariofilippo Jan 22, 2023
ee188d1
Merge branch 'main' into Feature_Cannot_Create_Shortcut_Dialog
QuaintMako Jan 24, 2023
7af7b3b
Strings
ferrariofilippo Jan 24, 2023
aa6cd6c
Merge branch 'main' into Feature_Cannot_Create_Shortcut_Dialog
ferrariofilippo Jan 24, 2023
bfe5618
Merge branch 'main' into Feature_Cannot_Create_Shortcut_Dialog
yaira2 Feb 1, 2023
d2a3d61
Update ShellFilesystemOperations.cs
yaira2 Feb 1, 2023
ca87b3f
Update ShellFilesystemOperations.cs
yaira2 Feb 1, 2023
6ab9604
Update UIFilesystemHelpers.cs
yaira2 Feb 1, 2023
c1f17dc
Update src/Files.App/Strings/en-US/Resources.resw
yaira2 Feb 1, 2023
f7dbb69
Update CreateShortcutDialogViewModel.cs
yaira2 Feb 1, 2023
4885899
Update CreateShortcutDialogViewModel.cs
yaira2 Feb 1, 2023
6faffcb
Update Resources.resw
yaira2 Feb 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/Files.App/Dialogs/CreateShortcutDialog.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@
Grid.Column="0"
HorizontalAlignment="Stretch"
PlaceholderText="C:\Users\"
Text="{x:Bind ViewModel.DestinationItemPath, Mode=TwoWay}"
TextChanged="DestinationItemPath_TextChanged" />
Text="{x:Bind ViewModel.DestinationItemPath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<Button
x:Name="SelectDestination"
Grid.Row="2"
Expand Down
27 changes: 0 additions & 27 deletions src/Files.App/Dialogs/CreateShortcutDialog.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,32 +22,5 @@ public CreateShortcutDialog()
}

public new async Task<DialogResult> ShowAsync() => (DialogResult)await base.ShowAsync();

private void DestinationItemPath_TextChanged(object sender, TextChangedEventArgs e)
{
if (string.IsNullOrWhiteSpace(DestinationItemPath.Text))
{
ViewModel.IsLocationValid = false;
return;
}

try
{
ViewModel.DestinationPathExists = Path.Exists(DestinationItemPath.Text) && DestinationItemPath.Text != Path.GetPathRoot(DestinationItemPath.Text);
if (ViewModel.DestinationPathExists)
{
ViewModel.IsLocationValid = true;
}
else
{
var uri = new Uri(DestinationItemPath.Text);
ViewModel.IsLocationValid = uri.IsWellFormedOriginalString();
}
}
catch (Exception)
{
ViewModel.IsLocationValid = false;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -273,28 +273,33 @@ public async Task<IStorageHistory> CreateShortcutItemsAsync(IList<IStorageItemWi

FileSystemProgress fsProgress = new(progress, true, FileSystemStatusCode.InProgress, source.Count);
fsProgress.Report();

var items = source.Zip(destination, (src, dest, index) => new { src, dest, index }).Where(x => !string.IsNullOrEmpty(x.src.Path) && !string.IsNullOrEmpty(x.dest));
foreach (var item in items)
{
if (await FileOperationsHelpers.CreateOrUpdateLinkAsync(item.dest, item.src.Path))
var result = await FileOperationsHelpers.CreateOrUpdateLinkAsync(item.dest, item.src.Path);

if (!result)
result = await UIFilesystemHelpers.HandleShortcutCannotBeCreated(Path.GetFileName(item.dest), item.src.Path);

if (result)
{
createdSources.Add(item.src);
createdDestination.Add(StorageHelpers.FromPathAndType(item.dest, FilesystemItemType.File));
}

fsProgress.ProcessedItemsCount = item.index;
fsProgress.Report();
}

fsProgress.ReportStatus(createdSources.Count == source.Count ? FileSystemStatusCode.Success : FileSystemStatusCode.Generic);

return new StorageHistory(FileOperationType.CreateLink, createdSources, createdDestination);
}

public async Task<IStorageHistory> DeleteAsync(IStorageItem source, IProgress<FileSystemProgress> progress, bool permanently, CancellationToken cancellationToken)
{
return await DeleteAsync(source.FromStorageItem(),
progress,
permanently,
cancellationToken);
return await DeleteAsync(source.FromStorageItem(), progress, permanently, cancellationToken);
}

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

FileSystemProgress fsProgress = new(progress, true, FileSystemStatusCode.InProgress);
fsProgress.Report();

var deleleFilePaths = source.Select(s => s.Path).Distinct();
var deleteFromRecycleBin = source.Any() && RecycleBinHelpers.IsPathUnderRecycleBin(source.ElementAt(0).Path);

permanently |= deleteFromRecycleBin;

if (deleteFromRecycleBin)
Expand All @@ -341,10 +348,12 @@ public async Task<IStorageHistory> DeleteItemsAsync(IList<IStorageItemWithPath>
if (result)
{
fsProgress.ReportStatus(FileSystemStatusCode.Success);

foreach (var item in deleteResult.Items)
{
await associatedInstance.FilesystemViewModel.RemoveFileOrFolderAsync(item.Source);
}

var recycledSources = deleteResult.Items.Where(x => x.Succeeded && x.Destination is not null && x.Source != x.Destination);
if (recycledSources.Any())
{
Expand All @@ -356,22 +365,22 @@ public async Task<IStorageHistory> DeleteItemsAsync(IList<IStorageItemWithPath>
await recycledSources.Zip(sourceMatch, (rSrc, oSrc) => new { rSrc, oSrc })
.Select(item => StorageHelpers.FromPathAndType(item.rSrc.Destination, item.oSrc.ItemType)).ToListAsync());
}

return new StorageHistory(FileOperationType.Delete, source, null);
}
else
{
if (deleteResult.Items.Any(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.Unauthorized))
{
if (await RequestAdminOperation())
{
return await DeleteItemsAsync(source, progress, permanently, cancellationToken);
}
}
else if (deleteResult.Items.Any(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.InUse))
{
var failedSources = deleteResult.Items.Where(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.InUse);
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
var lockingProcess = WhoIsLocking(filePath);

switch (await GetFileInUseDialog(filePath, lockingProcess))
{
case DialogResult.Primary:
Expand All @@ -391,20 +400,18 @@ public async Task<IStorageHistory> DeleteItemsAsync(IList<IStorageItemWithPath>
// Retry with StorageFile API
var failedSources = deleteResult.Items.Where(x => !x.Succeeded);
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();

return await filesystemOperations.DeleteItemsAsync(sourceMatch, progress, permanently, cancellationToken);
}
fsProgress.ReportStatus(CopyEngineResult.Convert(deleteResult.Items.FirstOrDefault(x => !x.Succeeded)?.HResult));

return null;
}
}

public async Task<IStorageHistory> MoveAsync(IStorageItem source, string destination, NameCollisionOption collision, IProgress<FileSystemProgress> progress, CancellationToken cancellationToken)
{
return await MoveAsync(source.FromStorageItem(),
destination,
collision,
progress,
cancellationToken);
return await MoveAsync(source.FromStorageItem(), destination, collision, progress, cancellationToken);
}

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

FileSystemProgress fsProgress = new(progress, true, FileSystemStatusCode.InProgress);
fsProgress.Report();

var sourceNoSkip = source.Zip(collisions, (src, coll) => new { src, coll }).Where(item => item.coll != FileNameConflictResolveOptionType.Skip).Select(item => item.src);
var destinationNoSkip = destination.Zip(collisions, (src, coll) => new { src, coll }).Where(item => item.coll != FileNameConflictResolveOptionType.Skip).Select(item => item.src);
var collisionsNoSkip = collisions.Where(c => c != FileNameConflictResolveOptionType.Skip);

var operationID = Guid.NewGuid().ToString();

using var r = cancellationToken.Register(CancelOperation, operationID, false);

var sourceReplace = sourceNoSkip.Zip(collisionsNoSkip, (src, coll) => new { src, coll }).Where(item => item.coll == FileNameConflictResolveOptionType.ReplaceExisting).Select(item => item.src);
var destinationReplace = destinationNoSkip.Zip(collisionsNoSkip, (src, coll) => new { src, coll }).Where(item => item.coll == FileNameConflictResolveOptionType.ReplaceExisting).Select(item => item.src);
var sourceRename = sourceNoSkip.Zip(collisionsNoSkip, (src, coll) => new { src, coll }).Where(item => item.coll != FileNameConflictResolveOptionType.ReplaceExisting).Select(item => item.src);
var destinationRename = destinationNoSkip.Zip(collisionsNoSkip, (src, coll) => new { src, coll }).Where(item => item.coll != FileNameConflictResolveOptionType.ReplaceExisting).Select(item => item.src);

var result = (FilesystemResult)true;
var moveResult = new ShellOperationResult();

if (sourceRename.Any())
{
var (status, response) = await FileOperationsHelpers.MoveItemAsync(sourceRename.Select(s => s.Path).ToArray(), destinationRename.ToArray(), false, NativeWinApiHelper.CoreWindowHandle.ToInt64(), operationID, progress);
Expand All @@ -461,16 +469,19 @@ public async Task<IStorageHistory> MoveItemsAsync(IList<IStorageItemWithPath> so
if (result)
{
fsProgress.ReportStatus(FileSystemStatusCode.Success);

var movedSources = moveResult.Items.Where(x => x.Succeeded && x.Destination is not null && x.Source != x.Destination);
if (movedSources.Any())
{
var sourceMatch = await movedSources.Select(x => sourceRename
.SingleOrDefault(s => s.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync();

return new StorageHistory(FileOperationType.Move,
sourceMatch,
await movedSources.Zip(sourceMatch, (rSrc, oSrc) => new { rSrc, oSrc })
.Select(item => StorageHelpers.FromPathAndType(item.rSrc.Destination, item.oSrc.ItemType)).ToListAsync());
}

return null; // Cannot undo overwrite operation
}
else
Expand All @@ -479,26 +490,27 @@ public async Task<IStorageHistory> MoveItemsAsync(IList<IStorageItemWithPath> so
if (moveResult.Items.Any(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.Unauthorized))
{
if (await RequestAdminOperation())
{
return await MoveItemsAsync(source, destination, collisions, progress, cancellationToken);
}
}
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)
{
var destName = subtree.dest.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries).Last();
var srcName = subtree.src.Path.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries).Last();

await DialogDisplayHelper.ShowDialogAsync("ErrorDialogThisActionCannotBeDone".GetLocalizedResource(), $"{"ErrorDialogTheDestinationFolder".GetLocalizedResource()} ({destName}) {"ErrorDialogIsASubfolder".GetLocalizedResource()} ({srcName})");
}
else if (moveResult.Items.Any(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.InUse))
{
var failedSources = moveResult.Items.Where(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.InUse);
var filePath = failedSources.Select(x => x.HResult == CopyEngineResult.COPYENGINE_E_SHARING_VIOLATION_SRC ? x.Source : x.Destination);
var lockingProcess = WhoIsLocking(filePath);

switch (await GetFileInUseDialog(filePath, lockingProcess))
{
case DialogResult.Primary:
var moveZip = sourceNoSkip.Zip(destinationNoSkip, (src, dest) => new { src, dest }).Zip(collisionsNoSkip, (z1, coll) => new { z1.src, z1.dest, coll });
var sourceMatch = await failedSources.Select(x => moveZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync();

return await MoveItemsAsync(
await sourceMatch.Select(x => x.src).ToListAsync(),
await sourceMatch.Select(x => x.dest).ToListAsync(),
Expand All @@ -511,6 +523,7 @@ await sourceMatch.Select(x => x.dest).ToListAsync(),
var failedSources = moveResult.Items.Where(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.NameTooLong);
var moveZip = sourceNoSkip.Zip(destinationNoSkip, (src, dest) => new { src, dest }).Zip(collisionsNoSkip, (z1, coll) => new { z1.src, z1.dest, coll });
var sourceMatch = await failedSources.Select(x => moveZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync();

return await filesystemOperations.MoveItemsAsync(
await sourceMatch.Select(x => x.src).ToListAsync(),
await sourceMatch.Select(x => x.dest).ToListAsync(),
Expand All @@ -530,11 +543,13 @@ await sourceMatch.Select(x => x.dest).ToListAsync(),
var failedSources = moveResult.Items.Where(x => !x.Succeeded);
var moveZip = sourceNoSkip.Zip(destinationNoSkip, (src, dest) => new { src, dest }).Zip(collisionsNoSkip, (z1, coll) => new { z1.src, z1.dest, coll });
var sourceMatch = await failedSources.Select(x => moveZip.SingleOrDefault(s => s.src.Path.Equals(x.Source, StringComparison.OrdinalIgnoreCase))).Where(x => x is not null).ToListAsync();

return await filesystemOperations.MoveItemsAsync(
await sourceMatch.Select(x => x.src).ToListAsync(),
await sourceMatch.Select(x => x.dest).ToListAsync(),
await sourceMatch.Select(x => x.coll).ToListAsync(), progress, cancellationToken);
}

return null;
}
}
Expand All @@ -554,41 +569,42 @@ public async Task<IStorageHistory> RenameAsync(IStorageItemWithPath source, stri

FileSystemProgress fsProgress = new(progress, true, FileSystemStatusCode.InProgress);
fsProgress.Report();
var renameResult = new ShellOperationResult();

var renameResult = new ShellOperationResult();
var (status, response) = await FileOperationsHelpers.RenameItemAsync(source.Path, newName, collision == NameCollisionOption.ReplaceExisting);

var result = (FilesystemResult)status;

renameResult.Items.AddRange(response?.Final ?? Enumerable.Empty<ShellOperationItemResult>());

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

if (result)
{
fsProgress.ReportStatus(FileSystemStatusCode.Success);

var renamedSources = renameResult.Items.Where(x => x.Succeeded && x.Destination is not null && x.Source != x.Destination)
.Where(x => new[] { source }.Select(s => s.Path).Contains(x.Source));
if (renamedSources.Any())
{
return new StorageHistory(FileOperationType.Rename, source,
StorageHelpers.FromPathAndType(renamedSources.Single().Destination, source.ItemType));
}

return null; // Cannot undo overwrite operation
}
else
{
if (renameResult.Items.Any(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.Unauthorized))
{
if (await RequestAdminOperation())
{
return await RenameAsync(source, newName, collision, progress, cancellationToken);
}
}
else if (renameResult.Items.Any(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.InUse))
{
var failedSources = renameResult.Items.Where(x => CopyEngineResult.Convert(x.HResult) == FileSystemStatusCode.InUse);
var filePath = failedSources.Select(x => x.HResult == CopyEngineResult.COPYENGINE_E_SHARING_VIOLATION_SRC ? x.Source : x.Destination);
var lockingProcess = WhoIsLocking(filePath);

switch (await GetFileInUseDialog(filePath, lockingProcess))
{
case DialogResult.Primary:
Expand All @@ -613,7 +629,9 @@ public async Task<IStorageHistory> RenameAsync(IStorageItemWithPath source, stri
// Retry with StorageFile API
return await filesystemOperations.RenameAsync(source, newName, collision, progress, cancellationToken);
}

fsProgress.ReportStatus(CopyEngineResult.Convert(renameResult.Items.FirstOrDefault(x => !x.Succeeded)?.HResult));

return null;
}
}
Expand Down
Loading