Skip to content

Commit 9a43c74

Browse files
authored
Fix: Fixed issue with pasting folders from Remote Desktop (#12516)
1 parent a1802c0 commit 9a43c74

File tree

5 files changed

+382
-8
lines changed

5 files changed

+382
-8
lines changed

src/Files.App/Filesystem/FilesystemOperations/Helpers/FilesystemHelpers.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22
// Licensed under the MIT License. See the LICENSE.
33

44
using Files.App.Filesystem.FilesystemHistory;
5+
using Files.App.Filesystem.StorageItems;
56
using Files.Backend.Services;
67
using Files.Backend.ViewModels.Dialogs.FileSystemDialog;
78
using Files.Sdk.Storage;
89
using Files.Shared.Services;
910
using Microsoft.Extensions.Logging;
1011
using System.IO;
1112
using System.Runtime.InteropServices;
13+
using System.Runtime.InteropServices.ComTypes;
1214
using Vanara.PInvoke;
15+
using Vanara.Windows.Shell;
1316
using Windows.ApplicationModel.DataTransfer;
1417
using Windows.Graphics.Imaging;
1518
using Windows.Storage;
@@ -733,6 +736,7 @@ public static bool HasDraggedStorageItems(DataPackageView packageView)
733736
public static async Task<IEnumerable<IStorageItemWithPath>> GetDraggedStorageItems(DataPackageView packageView)
734737
{
735738
var itemsList = new List<IStorageItemWithPath>();
739+
var hasVirtualItems = false;
736740

737741
if (packageView.Contains(StandardDataFormats.StorageItems))
738742
{
@@ -743,7 +747,7 @@ public static async Task<IEnumerable<IStorageItemWithPath>> GetDraggedStorageIte
743747
}
744748
catch (Exception ex) when ((uint)ex.HResult == 0x80040064 || (uint)ex.HResult == 0x8004006A)
745749
{
746-
// continue
750+
hasVirtualItems = true;
747751
}
748752
catch (Exception ex)
749753
{
@@ -752,6 +756,24 @@ public static async Task<IEnumerable<IStorageItemWithPath>> GetDraggedStorageIte
752756
}
753757
}
754758

759+
// workaround for pasting folders from remote desktop (#12318)
760+
if (hasVirtualItems && packageView.Contains("FileContents"))
761+
{
762+
var descriptor = NativeClipboard.CurrentDataObject.GetData<Shell32.FILEGROUPDESCRIPTOR>("FileGroupDescriptorW");
763+
for (var ii = 0; ii < descriptor.cItems; ii++)
764+
{
765+
if (descriptor.fgd[ii].dwFileAttributes.HasFlag(FileFlagsAndAttributes.FILE_ATTRIBUTE_DIRECTORY))
766+
{
767+
itemsList.Add(new VirtualStorageFolder(descriptor.fgd[ii].cFileName).FromStorageItem());
768+
}
769+
else if (NativeClipboard.CurrentDataObject.GetData("FileContents", DVASPECT.DVASPECT_CONTENT, ii) is IStream stream)
770+
{
771+
var streamContent = new ComStreamWrapper(stream);
772+
itemsList.Add(new VirtualStorageFile(streamContent, descriptor.fgd[ii].cFileName).FromStorageItem());
773+
}
774+
}
775+
}
776+
755777
// workaround for GetStorageItemsAsync() bug that only yields 16 items at most
756778
// https://learn.microsoft.com/windows/win32/shell/clipboard#cf_hdrop
757779
if (packageView.Contains("FileDrop"))

src/Files.App/Filesystem/StorageFileHelpers/FilesystemTasks.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ public static class FilesystemTasks
1414
{
1515
(UnauthorizedAccessException, _) => FileSystemStatusCode.Unauthorized,
1616
(FileNotFoundException, _) => FileSystemStatusCode.NotFound, // Item was deleted
17-
(COMException, _) => FileSystemStatusCode.NotFound, // Item's drive was ejected
18-
(_, 0x8007000F) => FileSystemStatusCode.NotFound, // The system cannot find the drive specified
1917
(PathTooLongException, _) => FileSystemStatusCode.NameTooLong,
2018
(FileAlreadyExistsException, _) => FileSystemStatusCode.AlreadyExists,
21-
(IOException, _) => FileSystemStatusCode.InUse,
22-
(ArgumentException, _) => ToStatusCode(T), // Item was invalid
19+
(_, 0x8007000F) => FileSystemStatusCode.NotFound, // The system cannot find the drive specified
2320
(_, 0x800700B7) => FileSystemStatusCode.AlreadyExists,
2421
(_, 0x80071779) => FileSystemStatusCode.ReadOnly,
22+
(COMException, _) => FileSystemStatusCode.NotFound, // Item's drive was ejected
23+
(IOException, _) => FileSystemStatusCode.InUse,
24+
(ArgumentException, _) => ToStatusCode(T), // Item was invalid
2525
_ => FileSystemStatusCode.Generic,
2626
};
2727

src/Files.App/Filesystem/StorageItems/StreamWithContentType.cs

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
// Copyright (c) 2023 Files Community
22
// Licensed under the MIT License. See the LICENSE.
33

4-
using System;
54
using System.IO;
5+
using System.Runtime.InteropServices;
6+
using System.Runtime.InteropServices.ComTypes;
67
using System.Runtime.InteropServices.WindowsRuntime;
7-
using System.Threading;
8-
using System.Threading.Tasks;
98
using Windows.Foundation;
109
using Windows.Storage.Streams;
1110

@@ -269,4 +268,72 @@ public void Dispose()
269268

270269
public string ContentType { get; set; } = "application/octet-stream";
271270
}
271+
272+
public class ComStreamWrapper : Stream
273+
{
274+
private IStream iStream;
275+
private STATSTG iStreamStat;
276+
277+
public ComStreamWrapper(IStream stream)
278+
{
279+
iStream = stream;
280+
iStream.Stat(out iStreamStat, 0);
281+
}
282+
283+
public override bool CanRead => true;
284+
285+
public override bool CanSeek => true;
286+
287+
public override bool CanWrite => false;
288+
289+
public override long Length => iStreamStat.cbSize;
290+
291+
public override long Position
292+
{
293+
get => Seek(0, SeekOrigin.Current);
294+
set => Seek(value, SeekOrigin.Begin);
295+
}
296+
297+
public override void Flush()
298+
{
299+
}
300+
301+
public override int Read(byte[] buffer, int offset, int count)
302+
{
303+
if (offset != 0)
304+
throw new NotSupportedException();
305+
unsafe
306+
{
307+
int newPos = 0;
308+
iStream.Read(buffer, count, new IntPtr(&newPos));
309+
return (int)newPos;
310+
}
311+
}
312+
313+
public override long Seek(long offset, SeekOrigin origin)
314+
{
315+
unsafe
316+
{
317+
long newPos = 0;
318+
iStream.Seek(0, (int)origin, new IntPtr(&newPos));
319+
return newPos;
320+
}
321+
}
322+
323+
public override void SetLength(long value)
324+
{
325+
throw new NotSupportedException();
326+
}
327+
328+
public override void Write(byte[] buffer, int offset, int count)
329+
{
330+
throw new NotSupportedException();
331+
}
332+
333+
protected override void Dispose(bool disposing)
334+
{
335+
base.Dispose(disposing);
336+
Marshal.ReleaseComObject(iStream);
337+
}
338+
}
272339
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
// Copyright (c) 2023 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
using System.IO;
5+
using System.Runtime.InteropServices.WindowsRuntime;
6+
using Windows.Foundation;
7+
using Windows.Storage;
8+
using Windows.Storage.FileProperties;
9+
using Windows.Storage.Streams;
10+
using IO = System.IO;
11+
12+
namespace Files.App.Filesystem.StorageItems
13+
{
14+
public class VirtualStorageFile : BaseStorageFile
15+
{
16+
public override string Path { get; }
17+
public override string Name { get; }
18+
public override string DisplayName => Name;
19+
public override string ContentType => "application/octet-stream";
20+
public override string FileType => IO.Path.GetExtension(Name);
21+
public override string FolderRelativeId => $"0\\{Name}";
22+
23+
public override string DisplayType
24+
{
25+
get
26+
{
27+
var itemType = "File".GetLocalizedResource();
28+
if (Name.Contains('.', StringComparison.Ordinal))
29+
{
30+
itemType = IO.Path.GetExtension(Name).Trim('.') + " " + itemType;
31+
}
32+
return itemType;
33+
}
34+
}
35+
36+
private Stream Contents { get; init; }
37+
38+
public override DateTimeOffset DateCreated { get; }
39+
public override Windows.Storage.FileAttributes Attributes { get; } = Windows.Storage.FileAttributes.Normal;
40+
public override IStorageItemExtraProperties Properties => new BaseBasicStorageItemExtraProperties(this);
41+
42+
public VirtualStorageFile(Stream contents, string cFileName)
43+
{
44+
Contents = contents;
45+
Name = cFileName;
46+
Path = "";
47+
}
48+
49+
private async void StreamedFileWriter(StreamedFileDataRequest request)
50+
{
51+
try
52+
{
53+
using (var stream = request.AsStreamForWrite())
54+
{
55+
await Contents.CopyToAsync(stream);
56+
await stream.FlushAsync();
57+
}
58+
request.Dispose();
59+
}
60+
catch (Exception)
61+
{
62+
request.FailAndClose(StreamedFileFailureMode.Incomplete);
63+
}
64+
}
65+
66+
public override IAsyncOperation<BaseBasicProperties> GetBasicPropertiesAsync()
67+
{
68+
return AsyncInfo.Run(async (cancellationToken) =>
69+
{
70+
return new BaseBasicProperties();
71+
});
72+
}
73+
74+
public override bool IsOfType(StorageItemTypes type)
75+
{
76+
return Attributes.HasFlag(Windows.Storage.FileAttributes.Directory) ? type == StorageItemTypes.Folder : type == StorageItemTypes.File;
77+
}
78+
79+
public override IAsyncOperation<StorageFile> ToStorageFileAsync()
80+
{
81+
return StorageFile.CreateStreamedFileAsync(Name, StreamedFileWriter, null);
82+
}
83+
84+
public override bool IsEqual(IStorageItem item) => item?.Path == Path;
85+
86+
public override IAsyncOperation<BaseStorageFolder> GetParentAsync() => throw new NotSupportedException();
87+
88+
public override IAsyncOperation<IRandomAccessStream> OpenAsync(FileAccessMode accessMode)
89+
{
90+
return Task.FromResult(Contents.AsRandomAccessStream()).AsAsyncOperation();
91+
}
92+
93+
public override IAsyncOperation<IRandomAccessStream> OpenAsync(FileAccessMode accessMode, StorageOpenOptions options) => OpenAsync(accessMode);
94+
95+
public override IAsyncOperation<IRandomAccessStreamWithContentType> OpenReadAsync()
96+
{
97+
return Task.FromResult<IRandomAccessStreamWithContentType>(new StreamWithContentType(Contents.AsRandomAccessStream()))
98+
.AsAsyncOperation();
99+
}
100+
101+
public override IAsyncOperation<IInputStream> OpenSequentialReadAsync()
102+
{
103+
return Task.FromResult(Contents.AsInputStream()).AsAsyncOperation();
104+
}
105+
106+
public override IAsyncOperation<StorageStreamTransaction> OpenTransactedWriteAsync() => throw new NotSupportedException();
107+
public override IAsyncOperation<StorageStreamTransaction> OpenTransactedWriteAsync(StorageOpenOptions options) => throw new NotSupportedException();
108+
109+
public override IAsyncOperation<BaseStorageFile> CopyAsync(IStorageFolder destinationFolder)
110+
=> CopyAsync(destinationFolder, Name, NameCollisionOption.FailIfExists);
111+
public override IAsyncOperation<BaseStorageFile> CopyAsync(IStorageFolder destinationFolder, string desiredNewName)
112+
=> CopyAsync(destinationFolder, desiredNewName, NameCollisionOption.FailIfExists);
113+
114+
public override IAsyncOperation<BaseStorageFile> CopyAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option)
115+
{
116+
return AsyncInfo.Run(async (cancellationToken) =>
117+
{
118+
BaseStorageFolder destFolder = destinationFolder.AsBaseStorageFolder();
119+
120+
if (destFolder is ICreateFileWithStream cwsf)
121+
{
122+
using var inStream = await this.OpenStreamForReadAsync();
123+
return await cwsf.CreateFileAsync(inStream, desiredNewName, option.Convert());
124+
}
125+
else
126+
{
127+
var destFile = await destFolder.CreateFileAsync(desiredNewName, option.Convert());
128+
using (var inStream = await this.OpenStreamForReadAsync())
129+
using (var outStream = await destFile.OpenStreamForWriteAsync())
130+
{
131+
await inStream.CopyToAsync(outStream);
132+
await outStream.FlushAsync();
133+
}
134+
return destFile;
135+
}
136+
});
137+
}
138+
139+
public override IAsyncAction MoveAsync(IStorageFolder destinationFolder) => throw new NotSupportedException();
140+
public override IAsyncAction MoveAsync(IStorageFolder destinationFolder, string desiredNewName) => throw new NotSupportedException();
141+
public override IAsyncAction MoveAsync(IStorageFolder destinationFolder, string desiredNewName, NameCollisionOption option) => throw new NotSupportedException();
142+
143+
public override IAsyncAction CopyAndReplaceAsync(IStorageFile fileToReplace) => throw new NotSupportedException();
144+
public override IAsyncAction MoveAndReplaceAsync(IStorageFile fileToReplace) => throw new NotSupportedException();
145+
146+
public override IAsyncAction RenameAsync(string desiredName)
147+
=> RenameAsync(desiredName, NameCollisionOption.FailIfExists);
148+
149+
public override IAsyncAction RenameAsync(string desiredName, NameCollisionOption option)
150+
=> throw new NotSupportedException();
151+
152+
public override IAsyncAction DeleteAsync() => throw new NotSupportedException();
153+
154+
public override IAsyncAction DeleteAsync(StorageDeleteOption option) => throw new NotSupportedException();
155+
156+
public override IAsyncOperation<StorageItemThumbnail> GetThumbnailAsync(ThumbnailMode mode)
157+
=> Task.FromResult<StorageItemThumbnail>(null).AsAsyncOperation();
158+
public override IAsyncOperation<StorageItemThumbnail> GetThumbnailAsync(ThumbnailMode mode, uint requestedSize)
159+
=> Task.FromResult<StorageItemThumbnail>(null).AsAsyncOperation();
160+
public override IAsyncOperation<StorageItemThumbnail> GetThumbnailAsync(ThumbnailMode mode, uint requestedSize, ThumbnailOptions options)
161+
=> Task.FromResult<StorageItemThumbnail>(null).AsAsyncOperation();
162+
}
163+
}

0 commit comments

Comments
 (0)