diff options
author | Tiago Conceição <Tiago_caza@hotmail.com> | 2021-11-14 22:57:01 +0300 |
---|---|---|
committer | Tiago Conceição <Tiago_caza@hotmail.com> | 2021-11-14 22:57:01 +0300 |
commit | 48c2c10cfd77005d9eb77a2042c23e916a700c16 (patch) | |
tree | 9f0f0cf3592f7bab92e9a3646e75a5279d04f551 | |
parent | 956e235d17165a6bbc518ff8a7c62edceb7a6514 (diff) |
Improvements on File -> Send to
- **File -> Send to -> Device**
- (Add) Progress with the transfered megabyte(s) and allow to cancel the transfer
- (Add) It will prompt for drive ejection [Configurable - On by default] [Windows only] (#340)
-rw-r--r-- | UVtools.Core/Extensions/NetworkExtensions.cs | 2 | ||||
-rw-r--r-- | UVtools.Core/Extensions/StreamExtensions.cs | 10 | ||||
-rw-r--r-- | UVtools.Core/Operations/OperationProgress.cs | 6 | ||||
-rw-r--r-- | UVtools.WPF/MainWindow.axaml.cs | 130 | ||||
-rw-r--r-- | UVtools.WPF/SystemOS/Windows/USB.cs | 130 | ||||
-rw-r--r-- | UVtools.WPF/UserSettings.cs | 7 | ||||
-rw-r--r-- | UVtools.WPF/Windows/SettingsWindow.axaml | 7 |
7 files changed, 272 insertions, 20 deletions
diff --git a/UVtools.Core/Extensions/NetworkExtensions.cs b/UVtools.Core/Extensions/NetworkExtensions.cs index cbe4844..d508705 100644 --- a/UVtools.Core/Extensions/NetworkExtensions.cs +++ b/UVtools.Core/Extensions/NetworkExtensions.cs @@ -40,7 +40,7 @@ namespace UVtools.Core.Extensions // Convert absolute progress (bytes downloaded) into relative progress (0% - 100%) var relativeProgress = new Progress<long>(downloadedBytes => progress.Report(new (contentLength.Value, downloadedBytes))); // Use extension method to report progress while downloading - await download.CopyToAsync(destination, 81920, relativeProgress, cancellationToken); + await download.CopyToAsync(destination, relativeProgress, cancellationToken); progress.Report(new(contentLength.Value, contentLength.Value)); return response; } diff --git a/UVtools.Core/Extensions/StreamExtensions.cs b/UVtools.Core/Extensions/StreamExtensions.cs index 2ef9fd6..985c8ae 100644 --- a/UVtools.Core/Extensions/StreamExtensions.cs +++ b/UVtools.Core/Extensions/StreamExtensions.cs @@ -15,6 +15,10 @@ namespace UVtools.Core.Extensions { public static class StreamExtensions { + //public const int DefaultCopyBufferSize = 81920; // 81.92 kilobytes, .NET default + //public const int DefaultCopyBufferSize = 512000; // 512 kilobytes + public const int DefaultCopyBufferSize = 1048576; // 1 MB + /// <summary> /// Converts stream into byte array /// </summary> @@ -39,6 +43,7 @@ namespace UVtools.Core.Extensions throw new ArgumentException("Has to be writable", nameof(destination)); if (bufferSize < 0) throw new ArgumentOutOfRangeException(nameof(bufferSize)); + if (bufferSize == 0) bufferSize = DefaultCopyBufferSize; var buffer = new byte[bufferSize]; long totalBytesRead = 0; @@ -50,5 +55,10 @@ namespace UVtools.Core.Extensions progress?.Report(totalBytesRead); } } + + public static async Task CopyToAsync(this Stream source, Stream destination, IProgress<long> progress = null, CancellationToken cancellationToken = default) + { + await CopyToAsync(source, destination, DefaultCopyBufferSize, progress, cancellationToken); + } } } diff --git a/UVtools.Core/Operations/OperationProgress.cs b/UVtools.Core/Operations/OperationProgress.cs index f96e1d0..a1a418f 100644 --- a/UVtools.Core/Operations/OperationProgress.cs +++ b/UVtools.Core/Operations/OperationProgress.cs @@ -179,6 +179,12 @@ namespace UVtools.Core.Operations RaisePropertyChanged(nameof(CanCancel)); } + public void ResetAll(string title, string name = "", uint itemCount = 0, uint items = 0) + { + Title = title; + Reset(name, itemCount, items); + } + public void Reset(string name = "", uint itemCount = 0, uint items = 0) { ItemName = name; diff --git a/UVtools.WPF/MainWindow.axaml.cs b/UVtools.WPF/MainWindow.axaml.cs index 64306f2..131f48e 100644 --- a/UVtools.WPF/MainWindow.axaml.cs +++ b/UVtools.WPF/MainWindow.axaml.cs @@ -625,7 +625,7 @@ namespace UVtools.WPF { if(drive.DriveType != DriveType.Removable || !drive.IsReady) continue; // Not our target, skip if (SlicerFile.FileFullPath.StartsWith(drive.Name)) continue; // File already on this device, skip - + var header = drive.Name; if (!string.IsNullOrWhiteSpace(drive.VolumeLabel)) { @@ -758,7 +758,7 @@ namespace UVtools.WPF } } - ShowProgressWindow($"Copying: {SlicerFile.Filename} to {path}", false); + ShowProgressWindow($"Copying: {SlicerFile.Filename} to {path}", true); Progress.ItemName = "Copying"; @@ -783,17 +783,17 @@ namespace UVtools.WPF Progress.ItemCount = (uint)(stream.Length / 1000000); - using var token = new CancellationTokenSource(); + bool isCopying = true; try { - new Task(() => + var task = new Task(() => { - while (!token.IsCancellationRequested) + while (isCopying) { Progress.ProcessedItems = (uint)(stream.Position / 1000000); Thread.Sleep(200); } - }, token.Token); + }); } catch (Exception) { @@ -801,7 +801,7 @@ namespace UVtools.WPF } response = await remotePrinter.RequestUploadFile.SendRequest(remotePrinter, Progress, SlicerFile.Filename, httpContent); - token.Cancel(); + isCopying = false; if (!response.IsSuccessStatusCode) { await this.MessageBoxError(response.ToString(), "Send to printer"); @@ -842,26 +842,120 @@ namespace UVtools.WPF } else { - await Task.Factory.StartNew(() => + /*var copyResult = await Task.Factory.StartNew(() => { try { - File.Copy(SlicerFile.FileFullPath, $"{Path.Combine(path, SlicerFile.Filename)}", true); - return true; + var fileDest = Path.Combine(path, SlicerFile.Filename); + //File.Copy(SlicerFile.FileFullPath, $"{Path.Combine(path, SlicerFile.Filename)}", true); + var buffer = new byte[1024 * 1024]; // 1MB buffer + + using var source = File.OpenRead(SlicerFile.FileFullPath); + using var dest = new FileStream(fileDest, FileMode.Create, FileAccess.Write); + //long totalBytes = 0; + //int currentBlockSize; + + Progress.Reset("Megabyte(s)", (uint)(source.Length / 1000000)); + var copyProgress = new Progress<long>(copiedBytes => Progress.ProcessedItems = (uint)(copiedBytes / 1000000)); + source.CopyToAsync(dest, 0, copyProgress, Progress.Token).ConfigureAwait(false); + + /*while ((currentBlockSize = source.Read(buffer)) > 0) + { + totalBytes += currentBlockSize; + + dest.Write(buffer, 0, currentBlockSize); + + if (Progress.Token.IsCancellationRequested) + { + // Delete dest file here + dest.Dispose(); + File.Delete(fileDest); + return false; + } + + Progress.ProcessedItems = (uint)(totalBytes / 1000000); + }*/ + + /* return true; + } + catch (OperationCanceledException) { } + catch (Exception exception) + { + Dispatcher.UIThread.InvokeAsync(async () => + await this.MessageBoxError(exception.ToString(), "Unable to copy the file")); + } + + return false; + });*/ + + bool copyResult = false; + var fileDest = Path.Combine(path, SlicerFile.Filename); + try + { + await using var source = File.OpenRead(SlicerFile.FileFullPath); + await using var dest = new FileStream(fileDest, FileMode.Create, FileAccess.Write); + + Progress.Reset("Megabyte(s)", (uint)(source.Length / 1000000)); + var copyProgress = new Progress<long>(copiedBytes => Progress.ProcessedItems = (uint)(copiedBytes / 1000000)); + await source.CopyToAsync(dest, copyProgress, Progress.Token); + + copyResult = true; + } + catch (OperationCanceledException) + { + try + { + if (File.Exists(fileDest)) File.Delete(fileDest); } - catch (OperationCanceledException) { } - catch (Exception exception) + catch (Exception ex) { - Dispatcher.UIThread.InvokeAsync(async () => - await this.MessageBoxError(exception.ToString(), "Unable to copy the file")); + Debug.WriteLine(ex); } + + } + catch (Exception exception) + { + await this.MessageBoxError(exception.ToString(), "Unable to copy the file"); + } - return false; - }); + if(copyResult && menuItem.Tag is DriveInfo removableDrive && OperatingSystem.IsWindows() && Settings.General.SendToPromptForRemovableDeviceEject) + { + if (await this.MessageBoxQuestion( + $"File '{SlicerFile.Filename}' has copied successfully into {removableDrive.Name}\n" + + $"Do you want to eject the {removableDrive.Name} drive now?", "Copied ok, eject the drive?") == ButtonResult.Yes) + { + Progress.ResetAll($"Ejecting {removableDrive.Name}"); + var ejectResult = await Task.Factory.StartNew(() => + { + try + { + return SystemOS.Windows.USB.USBEject(removableDrive.Name); + } + catch (OperationCanceledException) { } + catch (Exception exception) + { + Dispatcher.UIThread.InvokeAsync(async () => + await this.MessageBoxError(exception.ToString(), $"Unable to eject the drive {removableDrive.Name}")); + } + + return false; + }); + + if (!ejectResult) + { + await this.MessageBoxError($"Unable to eject the drive {removableDrive.Name}\n\n" + + "Possible causes:\n" + + "- Drive may be busy or locked\n" + + "- Drive was already ejected\n" + + "- No permission to eject the drive\n" + + "- Another error while trying to eject the drive\n\n" + + "Please try to eject the drive manually.", $"Unable to eject the drive {removableDrive.Name}"); + } + } + } } - IsGUIEnabled = true; } @@ -917,7 +1011,7 @@ namespace UVtools.WPF ShowLayer(); return; }*/ - } + } private void InitializeComponent() { diff --git a/UVtools.WPF/SystemOS/Windows/USB.cs b/UVtools.WPF/SystemOS/Windows/USB.cs new file mode 100644 index 0000000..4c96e59 --- /dev/null +++ b/UVtools.WPF/SystemOS/Windows/USB.cs @@ -0,0 +1,130 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; + +namespace UVtools.WPF.SystemOS.Windows +{ + public static class USB + { + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern IntPtr CreateFile( + string lpFileName, + uint dwDesiredAccess, + uint dwShareMode, + IntPtr SecurityAttributes, + uint dwCreationDisposition, + uint dwFlagsAndAttributes, + IntPtr hTemplateFile + ); + + [DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool DeviceIoControl( + IntPtr hDevice, + uint dwIoControlCode, + IntPtr lpInBuffer, + uint nInBufferSize, + IntPtr lpOutBuffer, + uint nOutBufferSize, + out uint lpBytesReturned, + IntPtr lpOverlapped + ); + + [DllImport("kernel32.dll", ExactSpelling = true, SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool DeviceIoControl( + IntPtr hDevice, + uint dwIoControlCode, + byte[] lpInBuffer, + uint nInBufferSize, + IntPtr lpOutBuffer, + uint nOutBufferSize, + out uint lpBytesReturned, + IntPtr lpOverlapped + ); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseHandle(IntPtr hObject); + + const uint GENERIC_READ = 0x80000000; + const uint GENERIC_WRITE = 0x40000000; + const int FILE_SHARE_READ = 0x1; + const int FILE_SHARE_WRITE = 0x2; + const int FSCTL_LOCK_VOLUME = 0x00090018; + const int FSCTL_DISMOUNT_VOLUME = 0x00090020; + const int IOCTL_STORAGE_EJECT_MEDIA = 0x2D4808; + const int IOCTL_STORAGE_MEDIA_REMOVAL = 0x002D4804; + + /// <summary> + /// Get the USB handler + /// </summary> + /// <param name="driveLetter">This should be the drive letter. Format: F:/, C:/..</param> + public static IntPtr GetUSBHandler(string driveLetter) + { + var filename = @"\\.\" + driveLetter[0] + ":"; + return CreateFile(filename, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, IntPtr.Zero, 0x3, 0, IntPtr.Zero); + } + + /// <summary> + /// Eject an USB drive + /// </summary> + /// <param name="driveLetter">This should be the drive letter. Format: F:/, C:/..</param> + public static bool USBEject(string driveLetter) + { + return Eject(GetUSBHandler(driveLetter)); + } + + public static bool Eject(IntPtr handle) + { + var result = false; + + if (LockVolume(handle) && DismountVolume(handle)) + { + PreventRemovalOfVolume(handle, false); + result = AutoEjectVolume(handle); + } + CloseHandle(handle); + return result; + } + + private static bool LockVolume(IntPtr handle, uint attempts = 10) + { + uint byteReturned; + + for (uint i = 0; i < attempts; i++) + { + if (DeviceIoControl(handle, FSCTL_LOCK_VOLUME, IntPtr.Zero, 0, IntPtr.Zero, 0, out byteReturned, IntPtr.Zero)) + { + return true; + } + Thread.Sleep(500); + } + return false; + } + + private static bool PreventRemovalOfVolume(IntPtr handle, bool prevent) + { + byte[] buf = new byte[1]; + uint retVal; + + buf[0] = (prevent) ? (byte)1 : (byte)0; + return DeviceIoControl(handle, IOCTL_STORAGE_MEDIA_REMOVAL, buf, 1, IntPtr.Zero, 0, out retVal, IntPtr.Zero); + } + + private static bool DismountVolume(IntPtr handle) + { + uint byteReturned; + return DeviceIoControl(handle, FSCTL_DISMOUNT_VOLUME, IntPtr.Zero, 0, IntPtr.Zero, 0, out byteReturned, IntPtr.Zero); + } + + private static bool AutoEjectVolume(IntPtr handle) + { + uint byteReturned; + return DeviceIoControl(handle, IOCTL_STORAGE_EJECT_MEDIA, IntPtr.Zero, 0, IntPtr.Zero, 0, out byteReturned, IntPtr.Zero); + } + + private static bool CloseVolume(IntPtr handle) + { + return CloseHandle(handle); + } + } +} diff --git a/UVtools.WPF/UserSettings.cs b/UVtools.WPF/UserSettings.cs index 7e62c94..4933ec6 100644 --- a/UVtools.WPF/UserSettings.cs +++ b/UVtools.WPF/UserSettings.cs @@ -54,6 +54,7 @@ namespace UVtools.WPF private bool _promptOverwriteFileSave = true; private string _fileSaveNamePrefix; private string _fileSaveNameSuffix = "_copy"; + private bool _sendToPromptForRemovableDeviceEject = true; private RangeObservableCollection<MappedDevice> _sendToCustomLocations = new(); @@ -175,6 +176,12 @@ namespace UVtools.WPF set => RaiseAndSetIfChanged(ref _fileSaveNameSuffix, value); } + public bool SendToPromptForRemovableDeviceEject + { + get => _sendToPromptForRemovableDeviceEject; + set => RaiseAndSetIfChanged(ref _sendToPromptForRemovableDeviceEject, value); + } + public RangeObservableCollection<MappedDevice> SendToCustomLocations { get => _sendToCustomLocations; diff --git a/UVtools.WPF/Windows/SettingsWindow.axaml b/UVtools.WPF/Windows/SettingsWindow.axaml index 16c3252..f7babc1 100644 --- a/UVtools.WPF/Windows/SettingsWindow.axaml +++ b/UVtools.WPF/Windows/SettingsWindow.axaml @@ -351,7 +351,12 @@ </Grid> <StackPanel Margin="10" Orientation="Vertical" Spacing="10"> - <DataGrid + + <CheckBox + IsChecked="{Binding Settings.General.SendToPromptForRemovableDeviceEject}" + Content="When send to a removable device, prompt for device ejection after copied [Windows only]"/> + + <DataGrid Name="SendToCustomLocationsGrid" CanUserReorderColumns="True" CanUserResizeColumns="True" |