windowsshellwinapicomole

Can I use IFileOperation with virtual files (IStream)?


I'm trying to copy files from a network stream to the local disk (with C#). I want to use the IFileOperation interface to get the modern copy UI, however I'm not sure how to get an IShellItem for a file that does not yet exist.

Currently I'm using the older IOperationsProgressDialog, which does work, however I'm wanting the Shell to deal with asking the user to overwrite files, permission issues, etc.

How do I create an IShellItem from an IStream to use in IFileOperation::Copy()?


Solution

  • IShellItem is a high level abstraction that's implemented by the Shell to avoid dealing with old interfaces like IShellFolder (which are not easy to work with). The problem is if you want to create a "virtual" one, you do need an IShellFolder somehow.

    That's what the code below does, creates a simple IShellFolder implementation just to ensure the Shell can create a "virtual" IShellItem over it.

    Exemple of console app (note the STAThread mandatory for IFileOperation to work)

    [STAThread]
    static void Main()
    {
        // prepare an item with a name
        var size = 50 * 200000;
        var item = new VirtualItem("test.txt", size, new DateTime(1966, 3, 24));
    
        item.Read += (s, e) =>
        {
            if (size == 0) // finished?
                return;
    
            // in this sample, we dont put anything in the buffer
            // we just wait to force the Shell progress dialog to show
            var written = Math.Min(e.Bytes.Length, size);
            e.Written = written;
            Thread.Sleep(100);
            size -= written;
        };
    
        // get source item as an IShellItem
        ShellNative.SHCreateItemWithParent(IntPtr.Zero, item.VirtualFolder, item.Pidl, typeof(ShellNative.IShellItem).GUID, out var source);
    
        // get some target folder as an IShellItem
        ShellNative.SHCreateItemFromParsingName(@"c:\temp", null, typeof(ShellNative.IShellItem).GUID, out var target);
    
        // create IFileOperation and ask to copy
        var fo = (ShellNative.IFileOperation)new ShellNative.FileOperation();
        fo.CopyItem((ShellNative.IShellItem)Marshal.GetObjectForIUnknown(source), (ShellNative.IShellItem)Marshal.GetObjectForIUnknown(target), null, IntPtr.Zero);
    
        // if you dont use of of these and the target file already exists
        // the whole process will fail for some reason
        const uint FOFX_DONTDISPLAYSOURCEPATH = 0x04000000;
        const uint FOFX_DONTDISPLAYLOCATIONS = 0x80000000;
        fo.SetOperationFlags(FOFX_DONTDISPLAYLOCATIONS);
    
        // do it
        fo.PerformOperations();
    }
    

    Note that if the target file already exists, you must set at least FOFX_DONTDISPLAYSOURCEPATH or FOFX_DONTDISPLAYLOCATIONS option otherwise the system will fail. The first one will show a dialog like this (where clicking on the compare choice does nothing):

    enter image description here

    And the second one will show a dialog like this (and you can customize what's shown as "The Path of 'text.txt'":

    enter image description here

    And the classes with all Shell interop:

    public sealed class VirtualItem : ShellNative.IPropertyStore, IStream, IDisposable
    {
        private readonly ComMemory _pidl;
    
        public event EventHandler<ReadEventArgs> Read;
    
        public VirtualItem(string name, long? size = null, DateTime? dateModified = null)
        {
            ArgumentNullException.ThrowIfNull(name);
            Name = name;
            VirtualFolder = new VirtualShellFolder(this);
            _pidl = new ComMemory(2);  // 4 bytes starting with 2 is the most simple non-terminator PIDL
            Size = size; // if size is unspecified, the dialog will work but info will be wrong
            DateModified = dateModified;
        }
    
        public string Name { get; }
        public long? Size { get; }
        public DateTime? DateModified { get; }
        public ShellNative.IShellFolder VirtualFolder { get; }
        public IntPtr Pidl => _pidl.Pointer;
    
        public void Dispose() => _pidl.Dispose();
    
        public int GetValue(ref ShellNative.PROPERTYKEY key, IntPtr pv)
        {
            if (key.Equals(ShellNative.PROPERTYKEY.PKEY_FileName))
            {
                ShellNative.WritePropVariant(Name, pv);
                return 0;
            }
    
            if (key.Equals(ShellNative.PROPERTYKEY.PKEY_ItemPathDisplay))
            {
                // this will be used when FOFX_DONTDISPLAYLOCATIONS is set in SetOperationFlags
                ShellNative.WritePropVariant(@"The Path of '" + Name + "'", pv);
                return 0;
            }
    
            // this is mostly used for the modified date
            if (key.Equals(ShellNative.PROPERTYKEY.PKEY_FindData))
            {
                var findData = new ShellNative.WIN32_FIND_DATAW();
                if (DateModified.HasValue)
                {
                    findData.ftLastWriteTime = ShellNative.ToFILETIME(DateModified.Value);
                }
    
                if (Size.HasValue)
                {
                    findData.nFileSizeLow = (uint)(Size.Value & uint.MaxValue);
                    findData.nFileSizeHigh = (uint)(Size.Value >> 32);
                }
    
                findData.cFileName = Name;
                using var mem = new ComMemory(findData);
                ShellNative.WritePropVariant(mem, pv);
                return 0;
            }
    
            // shell scans this to determine a root folder
            if (key.Equals(ShellNative.PROPERTYKEY.PKEY_Volume_IsRoot))
            {
                ShellNative.WritePropVariant(true, pv);
                return 0;
            }
    
            if (key.Equals(ShellNative.PROPERTYKEY.PKEY_Size) && Size.HasValue)
            {
                ShellNative.WritePropVariant(Size.Value, pv);
                return 0;
            }
    
            if (key.Equals(ShellNative.PROPERTYKEY.PKEY_DateModified) && DateModified.HasValue)
            {
                ShellNative.WritePropVariant(DateModified.Value, pv);
                return 0;
            }
    
            const int VT_EMPTY = 0;
            Marshal.WriteInt16(pv, VT_EMPTY);
            return 0;
        }
    
        void IStream.Read(byte[] pv, int cb, IntPtr pcbRead)
        {
            var evt = new ReadEventArgs(pv);
            Read?.Invoke(this, evt);
            if (pcbRead != IntPtr.Zero)
            {
                Marshal.WriteInt32(pcbRead, evt.Written);
            }
        }
    
        void IStream.Stat(out STATSTG pstatstg, int grfStatFlag)
        {
            pstatstg = new()
            {
                cbSize = Size ?? -1
            };
        }
    
        int ShellNative.IPropertyStore.GetCount(out int cProps) => throw new NotImplementedException();
        int ShellNative.IPropertyStore.GetAt(int iProp, out ShellNative.PROPERTYKEY pkey) => throw new NotImplementedException();
        int ShellNative.IPropertyStore.SetValue(ref ShellNative.PROPERTYKEY key, IntPtr propvar) => throw new NotImplementedException();
        int ShellNative.IPropertyStore.Commit() => throw new NotImplementedException();
        void IStream.Clone(out IStream ppstm) => throw new NotImplementedException();
        void IStream.Commit(int grfCommitFlags) => throw new NotImplementedException();
        void IStream.CopyTo(IStream pstm, long cb, IntPtr pcbRead, IntPtr pcbWritten) => throw new NotImplementedException();
        void IStream.LockRegion(long libOffset, long cb, int dwLockType) => throw new NotImplementedException();
        void IStream.Revert() => throw new NotImplementedException();
        void IStream.Seek(long dlibMove, int dwOrigin, IntPtr plibNewPosition) => throw new NotImplementedException();
        void IStream.SetSize(long libNewSize) => throw new NotImplementedException();
        void IStream.UnlockRegion(long libOffset, long cb, int dwLockType) => throw new NotImplementedException();
        void IStream.Write(byte[] pv, int cb, IntPtr pcbWritten) => throw new NotImplementedException();
    
        private sealed class VirtualShellFolder :
            ShellNative.IShellFolder2,
            ShellNative.IPersistIDList,
            ShellNative.IPersistFolder,
            ShellNative.IParentAndItem,
            ShellNative.IPropertyStoreFactory
        {
            private readonly VirtualItem _item;
    
            public VirtualShellFolder(VirtualItem item)
            {
                _item = item;
            }
    
            public int BindToObject(IntPtr pidl, IBindCtx pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv)
            {
                if (riid == typeof(IStream).GUID)
                    return ShellNative.QueryInterface(_item, riid, out ppv);
    
                return ShellNative.QueryInterface(this, riid, out ppv);
            }
    
            public int GetAttributesOf(int cidl, IntPtr apidl, ref int rgfInOut)
            {
                // our item is only a stream
                rgfInOut = ShellNative.SFGAO_STREAM;
                return 0;
            }
    
            public int GetDisplayNameOf(IntPtr pidl, ShellNative.SHGDNF uFlags, IntPtr pName)
            {
                // we dont honor full parsing name because ... we can't and that's probably why it fails in case of target file already exists
                ShellNative.WriteSTRRETString(pName, _item.Name);
                return 0;
            }
    
            public int GetIDList(out IntPtr ppidl)
            {
                ppidl = ShellNative.ILClone(_item.Pidl);
                return 0;
            }
    
            public int GetParentAndItem(IntPtr ppidlParent, IntPtr ppsf, IntPtr ppidlChild)
            {
                if (ppsf != IntPtr.Zero)
                {
                    ShellNative.QueryInterface(this, typeof(ShellNative.IShellFolder).GUID, out var ppv);
                    Marshal.WriteIntPtr(ppsf, ppv);
                }
    
                if (ppidlChild != IntPtr.Zero)
                {
                    var ppidl = ShellNative.ILClone(_item.Pidl);
                    Marshal.WriteIntPtr(ppidlChild, ppidl);
                }
                return 0;
            }
    
            public int GetDetailsEx(IntPtr pidl, ref ShellNative.PROPERTYKEY pscid, IntPtr pv) => _item.GetValue(ref pscid, pv);
            public int GetPropertyStore(int flags, IntPtr pUnkFactory, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv) => ShellNative.QueryInterface(_item, riid, out ppv);
            public int GetPropertyStoreForKeys(IntPtr rgKeys, int cKeys, int flags, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv) => ShellNative.QueryInterface(_item, riid, out ppv);
    
            public int Initialize(IntPtr pidl) => throw new NotImplementedException();
            public int ParseDisplayName(IntPtr hwnd, IBindCtx pbc, [MarshalAs(UnmanagedType.LPWStr)] string pszDisplayName, out int pchEaten, out IntPtr ppidl, IntPtr pdwAttributes) => throw new NotImplementedException();
            public int EnumObjects(IntPtr hwnd, int grfFlags, out IntPtr ppenumIDList) => throw new NotImplementedException();
            public int CompareIDs(long lParam, IntPtr pidl1, IntPtr pidl2) => throw new NotImplementedException();
            public int SetNameOf(IntPtr hwnd, IntPtr pidl, [MarshalAs(UnmanagedType.LPWStr)] string pszName, ShellNative.SHGDNF uFlags, out IntPtr ppidlOut) => throw new NotImplementedException();
            public int GetDefaultSearchGUID(out Guid pguid) => throw new NotImplementedException();
            public int EnumSearches(out IntPtr ppenum) => throw new NotImplementedException();
            public int GetDefaultColumn(int dwRes, out int pSort, out int pDisplay) => throw new NotImplementedException();
            public int GetDefaultColumnState(int iColumn, out int pcsFlags) => throw new NotImplementedException();
            public int SetParentAndItem(IntPtr pidlParent, ShellNative.IShellFolder psf, IntPtr pidlChild) => throw new NotImplementedException();
            public int SetIDList(IntPtr pidl) => throw new NotImplementedException();
            public int BindToStorage(IntPtr pidl, IBindCtx pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv) => throw new NotImplementedException();
            public int CreateViewObject(IntPtr hwndOwner, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv) => throw new NotImplementedException();
            public int GetDetailsOf(IntPtr pidl, int iColumn, IntPtr psd) => throw new NotImplementedException();
            public int MapColumnToSCID(int iColumn, ref ShellNative.PROPERTYKEY pscid) => throw new NotImplementedException();
            public int GetClassID(out Guid pClassID) => throw new NotImplementedException();
            public int GetUIObjectOf(IntPtr hwndOwner, int cidl, IntPtr apidl, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, IntPtr rgfReserved, out IntPtr ppv) => throw new NotImplementedException();
        }
    }
    
    public sealed class ReadEventArgs : EventArgs
    {
        public ReadEventArgs(byte[] bytes)
        {
            Bytes = bytes;
        }
    
        public byte[] Bytes { get; }
        public int Written { get; set; }
    }
    
    public sealed class ComMemory : IDisposable
    {
        private IntPtr _pointer;
        public ComMemory(object structure) { Size = Marshal.SizeOf(structure); _pointer = Marshal.AllocCoTaskMem(Size); Marshal.StructureToPtr(structure, _pointer, false); }
        public IntPtr Pointer => _pointer;
        public int Size { get; }
        public void Dispose() { var handle = Interlocked.Exchange(ref _pointer, IntPtr.Zero); if (handle != IntPtr.Zero) Marshal.FreeCoTaskMem(handle); }
    }
    
    public static class ShellNative
    {
        public static void WriteSTRRETString(IntPtr ptr, string name)
        {
            if (ptr == IntPtr.Zero)
                return;
    
            // implicit STRRET_TYPE is 0 => STRRET_WSTR
            ZeroMemory(ptr, IntPtr.Size == 8 ? Marshal.SizeOf<STRRET_64>() : Marshal.SizeOf<STRRET_32>());
            if (name == null)
                return;
    
            var strPtr = Marshal.StringToCoTaskMemUni(name);
    
            // skip uType wich size + padding = 4 or 8
            Marshal.WriteIntPtr(ptr + IntPtr.Size, strPtr);
        }
    
        public static FILETIME ToFILETIME(DateTime dt) { var ft = dt.ToFileTime(); return new FILETIME { dwLowDateTime = (int)(ft & uint.MaxValue), dwHighDateTime = (int)(ft >> 32) }; }
    
        public static int QueryInterface(object obj, Guid iid, out IntPtr ppv)
        {
            ppv = IntPtr.Zero;
            if (obj == null)
                return E_NOINTERFACE;
    
            var unk = Marshal.GetIUnknownForObject(obj);
            try
            {
                return Marshal.QueryInterface(unk, ref iid, out ppv);
            }
            finally
            {
                Marshal.Release(unk);
            }
        }
    
        public static void WritePropVariant(object obj, IntPtr pv)
        {
            if (obj is string s)
            {
                var ptr = Marshal.StringToCoTaskMemUni(s);
                Marshal.WriteIntPtr(pv + 8, ptr);
                const int VT_LPWSTR = 31;
                Marshal.WriteInt16(pv, VT_LPWSTR);
                return;
            }
    
            if (obj is bool b)
            {
                Marshal.WriteInt16(pv + 8, (short)(b ? -1 : 0));
                const int VT_BOOL = 11;
                Marshal.WriteInt16(pv, VT_BOOL);
                return;
            }
    
            if (obj is long l)
            {
                Marshal.WriteInt64(pv + 8, l);
                const int VT_UI8 = 21;
                Marshal.WriteInt16(pv, VT_UI8);
                return;
            }
    
            if (obj is DateTime dt)
            {
                InitPropVariantFromFileTime(dt.ToFileTime(), pv);
                return;
            }
    
            if (obj is ComMemory mem)
            {
                var hr = InitPropVariantFromBuffer(mem.Pointer, mem.Size, pv);
                return;
            }
            throw new NotSupportedException();
        }
    
        [DllImport("kernel32", EntryPoint = "RtlZeroMemory")]
        public static extern void ZeroMemory(IntPtr address, IntPtr length);
        public static void ZeroMemory(IntPtr address, int length) => ZeroMemory(address, (IntPtr)length);
    
        [DllImport("shell32")]
        public static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string pszPath, IBindCtx pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv);
    
        [DllImport("shell32")]
        public static extern int SHCreateItemWithParent(IntPtr pidlParent, IShellFolder psfParent, IntPtr pidl, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv);
    
        [DllImport("propsys")]
        public static extern int InitPropVariantFromBuffer(IntPtr pv, int cb, IntPtr ppropvar);
    
        [DllImport("propsys")]
        public static extern int InitPropVariantFromFileTime([MarshalAs(UnmanagedType.LPStruct)] long pftIn, IntPtr ppropvar);
    
        [DllImport("shell32")]
        public static extern IntPtr ILClone(IntPtr pidl);
    
        [ComImport, Guid("000214e6-0000-0000-c000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        public interface IShellFolder
        {
            [PreserveSig] int ParseDisplayName(IntPtr hwnd, IBindCtx pbc, [MarshalAs(UnmanagedType.LPWStr)] string pszDisplayName, out int pchEaten, out IntPtr ppidl, IntPtr pdwAttributes);
            [PreserveSig] int EnumObjects(IntPtr hwnd, int grfFlags, out IntPtr ppenumIDList);
            [PreserveSig] int BindToObject(IntPtr pidl, IBindCtx pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv);
            [PreserveSig] int BindToStorage(IntPtr pidl, IBindCtx pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv);
            [PreserveSig] int CompareIDs(long lParam, IntPtr pidl1, IntPtr pidl2);
            [PreserveSig] int CreateViewObject(IntPtr hwndOwner, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv);
            [PreserveSig] int GetAttributesOf(int cidl, IntPtr apidl, ref int rgfInOut);
            [PreserveSig] int GetUIObjectOf(IntPtr hwndOwner, int cidl, IntPtr apidl, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, IntPtr rgfReserved, out IntPtr ppv);
            [PreserveSig] int GetDisplayNameOf(IntPtr pidl, SHGDNF uFlags, IntPtr pName);
            [PreserveSig] int SetNameOf(IntPtr hwnd, IntPtr pidl, [MarshalAs(UnmanagedType.LPWStr)] string pszName, SHGDNF uFlags, out IntPtr ppidlOut);
        }
    
        [ComImport, Guid("93f2f68c-1d1b-11d3-a30e-00c04f79abd1"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        public interface IShellFolder2 : IShellFolder
        {
            [PreserveSig] new int ParseDisplayName(IntPtr hwnd, IBindCtx pbc, [MarshalAs(UnmanagedType.LPWStr)] string pszDisplayName, out int pchEaten, out IntPtr ppidl, IntPtr pdwAttributes);
            [PreserveSig] new int EnumObjects(IntPtr hwnd, int grfFlags, out IntPtr ppenumIDList);
            [PreserveSig] new int BindToObject(IntPtr pidl, IBindCtx pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv);
            [PreserveSig] new int BindToStorage(IntPtr pidl, IBindCtx pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv);
            [PreserveSig] new int CompareIDs(long lParam, IntPtr pidl1, IntPtr pidl2);
            [PreserveSig] new int CreateViewObject(IntPtr hwndOwner, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv);
            [PreserveSig] new int GetAttributesOf(int cidl, IntPtr apidl, ref int rgfInOut);
            [PreserveSig] new int GetUIObjectOf(IntPtr hwndOwner, int cidl, IntPtr apidl, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, IntPtr rgfReserved, out IntPtr ppv);
            [PreserveSig] new int GetDisplayNameOf(IntPtr pidl, SHGDNF uFlags, IntPtr pName);
            [PreserveSig] new int SetNameOf(IntPtr hwnd, IntPtr pidl, [MarshalAs(UnmanagedType.LPWStr)] string pszName, SHGDNF uFlags, out IntPtr ppidlOut);
            [PreserveSig] int GetDefaultSearchGUID(out Guid pguid);
            [PreserveSig] int EnumSearches(out IntPtr ppenum);
            [PreserveSig] int GetDefaultColumn(int dwRes, out int pSort, out int pDisplay);
            [PreserveSig] int GetDefaultColumnState(int iColumn, out int pcsFlags);
            [PreserveSig] int GetDetailsEx(IntPtr pidl, ref ShellNative.PROPERTYKEY pscid, IntPtr pv);
            [PreserveSig] int GetDetailsOf(IntPtr pidl, int iColumn, IntPtr psd);
            [PreserveSig] int MapColumnToSCID(int iColumn, ref ShellNative.PROPERTYKEY pscid);
        }
    
        [ComImport, Guid("0000010c-0000-0000-c000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        public interface IPersist
        {
            [PreserveSig] int GetClassID(out Guid pClassID);
        }
    
        [ComImport, Guid("1079acfc-29bd-11d3-8e0d-00c04f6837d5"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        public interface IPersistIDList : IPersist
        {
            [PreserveSig] new int GetClassID(out Guid pClassID);
            [PreserveSig] int SetIDList(IntPtr pidl);
            [PreserveSig] int GetIDList(out IntPtr ppidl);
        }
    
        [ComImport, Guid("000214ea-0000-0000-c000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        public interface IPersistFolder : IPersist
        {
            [PreserveSig] new int GetClassID(out Guid pClassID);
            [PreserveSig] int Initialize(IntPtr pidl);
        }
    
        [ComImport, Guid("886d8eeb-8cf2-4446-8d02-cdba1dbdcf99"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        public interface IPropertyStore
        {
            [PreserveSig] int GetCount(out int cProps);
            [PreserveSig] int GetAt(int iProp, out PROPERTYKEY pkey);
            [PreserveSig] int GetValue(ref PROPERTYKEY key, IntPtr pv);
            [PreserveSig] int SetValue(ref PROPERTYKEY key, IntPtr propvar);
            [PreserveSig] int Commit();
        }
    
        [ComImport, Guid("bc110b6d-57e8-4148-a9c6-91015ab2f3a5"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        public interface IPropertyStoreFactory
        {
            [PreserveSig] int GetPropertyStore(int flags, IntPtr pUnkFactory, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv);
            [PreserveSig] int GetPropertyStoreForKeys(IntPtr rgKeys, int cKeys, int flags, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IntPtr ppv);
        }
    
        [ComImport, Guid("b3a4b685-b685-4805-99d9-5dead2873236"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        public interface IParentAndItem
        {
            [PreserveSig] int SetParentAndItem(IntPtr pidlParent, IShellFolder psf, IntPtr pidlChild);
            [PreserveSig] int GetParentAndItem(IntPtr ppidlParent, IntPtr ppsf, IntPtr ppidlChild);
        }
    
        [ComImport, Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        public interface IShellItem
        {
            // undefined as we dont need methods
        }
    
        [Guid("947aab5f-0a5c-4c13-b4d6-4bf7836fc9f8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        public interface IFileOperation
        {
            [PreserveSig] int Advise(IntPtr pfops, out int pdwCookie);
            [PreserveSig] int Unadvise(int dwCookie);
            [PreserveSig] int SetOperationFlags(uint dwOperationFlags);
            [PreserveSig] int SetProgressMessage([MarshalAs(UnmanagedType.LPWStr)] string pszMessage);
            [PreserveSig] int SetProgressDialog(IntPtr popd);
            [PreserveSig] int SetProperties(IntPtr pproparray);
            [PreserveSig] int SetOwnerWindow(IntPtr hwndOwner);
            [PreserveSig] int ApplyPropertiesToItem(IShellItem psiItem);
            [PreserveSig] int ApplyPropertiesToItems([MarshalAs(UnmanagedType.IUnknown)] object punkItems);
            [PreserveSig] int RenameItem(IShellItem psiItem, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, IntPtr pfopsItem);
            [PreserveSig] int RenameItems([MarshalAs(UnmanagedType.IUnknown)] object pUnkItems, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName);
            [PreserveSig] int MoveItem(IShellItem psiItem, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszNewName, IntPtr pfopsItem);
            [PreserveSig] int MoveItems([MarshalAs(UnmanagedType.IUnknown)] object punkItems, IShellItem psiDestinationFolder);
            [PreserveSig] int CopyItem(IShellItem psiItem, IShellItem psiDestinationFolder, [MarshalAs(UnmanagedType.LPWStr)] string pszCopyName, IntPtr pfopsItem);
            [PreserveSig] int CopyItems([MarshalAs(UnmanagedType.IUnknown)] object punkItems, IShellItem psiDestinationFolder);
            [PreserveSig] int DeleteItem(IShellItem psiItem, IntPtr pfopsItem);
            [PreserveSig] int DeleteItems([MarshalAs(UnmanagedType.IUnknown)] object punkItems);
            [PreserveSig] int NewItem(IShellItem psiDestinationFolder, uint dwFileAttributes, [MarshalAs(UnmanagedType.LPWStr)] string pszName, [MarshalAs(UnmanagedType.LPWStr)] string pszTemplateName, IntPtr pfopsItem);
            [PreserveSig] int PerformOperations();
            [PreserveSig] int GetAnyOperationsAborted(out bool pfAnyOperationsAborted);
        }
    
        [ComImport, Guid("3ad05575-8857-4850-9277-11b85bdb8e09")] // CLSID_FileOperation
        public class FileOperation { }
    
        [StructLayout(LayoutKind.Sequential)]
        public struct PROPERTYKEY : IEquatable<PROPERTYKEY>
        {
            public Guid fmtid { get; }
            public int pid { get; }
            public PROPERTYKEY(Guid fmtid, int pid) { this.fmtid = fmtid; this.pid = pid; }
            public override string ToString() => fmtid.ToString("B") + " " + pid;
            public override int GetHashCode() => fmtid.GetHashCode() ^ pid;
            public bool Equals(PROPERTYKEY other) => other.fmtid == fmtid && other.pid == pid;
            public override bool Equals(object obj) => obj is PROPERTYKEY other && Equals(other);
    
            public static PROPERTYKEY PKEY_DateModified { get; } = new PROPERTYKEY(new Guid("b725f130-47ef-101a-a5f1-02608c9eebac"), 14);
            public static PROPERTYKEY PKEY_FileName { get; } = new PROPERTYKEY(new Guid("41cf5ae0-f75a-4806-bd87-59c7d9248eb9"), 100);
            public static PROPERTYKEY PKEY_FindData { get; } = new PROPERTYKEY(new Guid("28636aa6-953d-11d2-b5d6-00c04fd918d0"), 0);
            public static PROPERTYKEY PKEY_ItemPathDisplay { get; } = new PROPERTYKEY(new Guid("e3e0584c-b788-4a5a-bb20-7f5a44c9acdd"), 7);
            public static PROPERTYKEY PKEY_Size { get; } = new PROPERTYKEY(new Guid("b725f130-47ef-101a-a5f1-02608c9eebac"), 12);
            public static PROPERTYKEY PKEY_Volume_IsRoot { get; } = new PROPERTYKEY(new Guid("9b174b35-40ff-11d2-a27e-00c04fc30871"), 10);
        }
    
        [StructLayout(LayoutKind.Explicit, Size = 264)]
        public struct STRRET_32
        {
            [FieldOffset(0)]
            public STRRET_TYPE uType;
    
            [FieldOffset(4)]
            public IntPtr pOleStr;
    
            [FieldOffset(4)]
            public uint uOffset;
    
            [FieldOffset(4)]
            public IntPtr cStr;
        }
    
        [StructLayout(LayoutKind.Explicit, Size = 272)]
        public struct STRRET_64
        {
            [FieldOffset(0)]
            public STRRET_TYPE uType;
    
            [FieldOffset(8)]
            public IntPtr pOleStr;
    
            [FieldOffset(8)]
            public uint uOffset;
    
            [FieldOffset(8)]
            public IntPtr cStr;
        }
    
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        public struct WIN32_FIND_DATAW
        {
            public int dwFileAttributes;
            public FILETIME ftCreationTime;
            public FILETIME ftLastAccessTime;
            public FILETIME ftLastWriteTime;
            public uint nFileSizeHigh;
            public uint nFileSizeLow;
            public int dwReserved0;
            public int dwReserved1;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
            public string cFileName;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
            public string cAlternateFileName;
        }
    
        public enum STRRET_TYPE
        {
            STRRET_WSTR = 0,
            STRRET_OFFSET = 1,
            STRRET_CSTR = 2,
        }
    
        [Flags]
        public enum SHGDNF
        {
            SHGDN_NORMAL = 0x0,
            SHGDN_INFOLDER = 0x1,
            SHGDN_FOREDITING = 0x1000,
            SHGDN_FORADDRESSBAR = 0x4000,
            SHGDN_FORPARSING = 0x8000,
        }
    
        public const int SFGAO_STREAM = 0x00400000;
        public const int E_NOTIMPL = unchecked((int)0x80004001);
        public const int E_NOINTERFACE = unchecked((int)0x80004002);
    }