498 lines
16 KiB
C#
498 lines
16 KiB
C#
/*
|
|
* VirtualFS - Virtual File System library.
|
|
* Copyright (c) 2021, Grzegorz Russek (grzegorz.russek@gmail.com)
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are met:
|
|
*
|
|
* Redistributions of source code must retain the above copyright notice,
|
|
* this list of conditions and the following disclaimer.
|
|
*
|
|
* Redistributions in binary form must reproduce the above copyright notice,
|
|
* this list of conditions and the following disclaimer in the documentation
|
|
* and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
using Renci.SshNet;
|
|
using Renci.SshNet.Sftp;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text.RegularExpressions;
|
|
using VirtualFS.Base;
|
|
|
|
namespace VirtualFS.Physical
|
|
{
|
|
public class SFtpFileSystem : BaseFileSystem
|
|
{
|
|
private string _ftpServer;
|
|
private string _userName;
|
|
private string _password;
|
|
private byte[] _key;
|
|
private byte[] _fingerPrint;
|
|
private string _rootPath;
|
|
private Action<Exception> _exHandler;
|
|
private SftpClient _client;
|
|
|
|
private SftpClient Client
|
|
{
|
|
get
|
|
{
|
|
if (_client == null)
|
|
_client = CreateClientPrivate();
|
|
|
|
return _client;
|
|
}
|
|
}
|
|
|
|
/// <summary>Initializes a new instance of the <see cref="SFtpFileSystem"/> class.</summary>
|
|
/// <param name="host">The SFTP server.</param>
|
|
/// <param name="userName">SFTP User name.</param>
|
|
/// <param name="password">SFTP password.</param>
|
|
/// <param name="key">SFTP private key.</param>
|
|
/// <param name="fingerPrint">SFTP server fingerprint.</param>
|
|
/// <param name="rootPath">SFTP root path on server.</param>
|
|
public SFtpFileSystem(string host, string userName, string password, byte[] key, byte[] fingerPrint, string rootPath, Action<Exception> exHandler = null)
|
|
: base(false)
|
|
{
|
|
_exHandler = exHandler ?? (e => { });
|
|
_ftpServer = host;
|
|
_userName = userName;
|
|
_password = password;
|
|
_key = key;
|
|
_fingerPrint = fingerPrint;
|
|
_rootPath = rootPath;
|
|
|
|
if (_ftpServer.EndsWith("/"))
|
|
_ftpServer = _ftpServer.Substring(0, _ftpServer.Length - 1);
|
|
|
|
if (!string.IsNullOrEmpty(_rootPath) && _rootPath.EndsWith("/"))
|
|
_rootPath = _rootPath.Substring(0, _rootPath.Length - 1);
|
|
|
|
if (_rootPath == "/")
|
|
_rootPath = string.Empty;
|
|
}
|
|
|
|
private SftpClient CreateClientPrivate()
|
|
{
|
|
var ams = new List<AuthenticationMethod>();
|
|
|
|
if (!string.IsNullOrEmpty(_password))
|
|
ams.Add(new PasswordAuthenticationMethod(_userName, _password));
|
|
|
|
if (_key != null && _key.Length > 0)
|
|
using (var ms = new MemoryStream(_key))
|
|
ams.Add(new PrivateKeyAuthenticationMethod(_userName, new PrivateKeyFile(ms)));
|
|
|
|
var ci = new ConnectionInfo(_ftpServer, _userName, ams.ToArray());
|
|
|
|
var client = new SftpClient(ci);
|
|
|
|
if (_fingerPrint != null && _fingerPrint.Length > 0)
|
|
client.HostKeyReceived += (sender, e) =>
|
|
{
|
|
e.CanTrust = true;
|
|
|
|
if (_fingerPrint.Length == e.FingerPrint.Length)
|
|
for (var i = 0; i < _fingerPrint.Length; i++)
|
|
if (_fingerPrint[i] != e.FingerPrint[i])
|
|
{
|
|
e.CanTrust = false;
|
|
break;
|
|
}
|
|
else
|
|
e.CanTrust = false;
|
|
};
|
|
else
|
|
client.HostKeyReceived += (sender, e) => e.CanTrust = true;
|
|
|
|
client.Connect();
|
|
|
|
return client;
|
|
}
|
|
|
|
private Entry CreateEntry(Path path, ISftpFile ls, bool pathIsParent = true)
|
|
{
|
|
Entry e = null;
|
|
if (ls.IsDirectory)
|
|
e = new Directory
|
|
{
|
|
IsReadOnly = IsReadOnly,
|
|
Path = pathIsParent ? (path + (ls.Name + "/")) : path,
|
|
FileSystem = this,
|
|
Data = ls,
|
|
};
|
|
else
|
|
e = new File
|
|
{
|
|
CreationTime = ls.LastWriteTime,
|
|
LastWriteTime = ls.LastWriteTime,
|
|
LastAccessTime = ls.LastAccessTime,
|
|
Size = ls.Length,
|
|
IsReadOnly = IsReadOnly,
|
|
Path = pathIsParent ? (path + ls.Name) : path,
|
|
FileSystem = this,
|
|
Data = ls,
|
|
};
|
|
return e;
|
|
}
|
|
|
|
private string AdaptPath(SftpClient c, Path path)
|
|
{
|
|
var r = path.ToString().Substring(1);
|
|
|
|
if (!string.IsNullOrEmpty(_rootPath)) {
|
|
r = (string)(_rootPath + (string)path);
|
|
|
|
if (string.IsNullOrEmpty(r))
|
|
return null;
|
|
|
|
r = r[0] == Path.SeparatorChar ? r.Substring(1) : r;
|
|
}
|
|
|
|
var wd = c.WorkingDirectory;
|
|
|
|
if (!wd.EndsWith(Path.SeparatorChar.ToString()))
|
|
wd += Path.SeparatorChar;
|
|
|
|
return wd + r;
|
|
}
|
|
|
|
/// <summary>Get path to physical file containing entry.</summary>
|
|
/// <param name="path">Virtual file system path.</param>
|
|
/// <returns>Real file system path or <c>null</c> if real
|
|
/// path doesn't exist.</returns>
|
|
/// <remarks>This may not work with all file systems. Implementation
|
|
/// should return null if real path can't be determined.</remarks>
|
|
public override string EntryRealPath(Path path)
|
|
{
|
|
string uri = _ftpServer;
|
|
|
|
if (!string.IsNullOrEmpty(_rootPath))
|
|
uri += _rootPath;
|
|
|
|
if (path.Parent != null)
|
|
uri += (string)path.Parent + (string)path.Name ?? string.Empty;
|
|
|
|
return uri;
|
|
}
|
|
|
|
/// <summary>Check if given path exists.</summary>
|
|
/// <param name="path">Path to check.</param>
|
|
/// <returns>Returns <c>true</c> if entry does
|
|
/// exist, otherwise <c>false</c>.</returns>
|
|
public override bool Exists(Path path)
|
|
{
|
|
try
|
|
{
|
|
return Client.Exists(AdaptPath(Client, path));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_exHandler(ex);
|
|
|
|
if (_client != null)
|
|
{
|
|
_client.Dispose();
|
|
_client = null;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>Get entries located under given path.</summary>
|
|
/// <param name="path">Path to get.</param>
|
|
/// <param name="mask">Mask to filter out unwanted entries.</param>
|
|
/// <returns>Returns entry information.</returns>
|
|
public override IEnumerable<Entry> GetEntries(Path path, Regex mask = null)
|
|
{
|
|
if (!path.IsDirectory)
|
|
throw new InvalidOperationException(string.Format("Path '{0}' is not a directory, thus it has no entries.", path));
|
|
|
|
List<Entry> result = new List<Entry>();
|
|
|
|
try
|
|
{
|
|
var lst = Client.ListDirectory(AdaptPath(Client, path));
|
|
|
|
foreach (var ls in lst)
|
|
{
|
|
if (ls.Name == "." || ls.Name == "..")
|
|
continue;
|
|
|
|
Entry e = CreateEntry(path, ls);
|
|
|
|
if (e != null)
|
|
result.Add(e);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_exHandler(ex);
|
|
|
|
if (_client != null)
|
|
{
|
|
_client.Dispose();
|
|
_client = null;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>Get entry located under given path.</summary>
|
|
/// <param name="path">Path to get.</param>
|
|
/// <returns>Returns entry information.</returns>
|
|
public override Entry GetEntry(Path path)
|
|
{
|
|
Entry result = null;
|
|
|
|
try
|
|
{
|
|
if (Client.Exists(AdaptPath(Client, path)))
|
|
result = CreateEntry(path, Client.Get(AdaptPath(Client, path)), false);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>Change name of the entry under specified path.</summary>
|
|
/// <param name="path">The path to entry which name will be changed.</param>
|
|
/// <param name="newName">The new name of entry.</param>
|
|
public override void ReName(Path path, string newName)
|
|
{
|
|
base.ReName(path, newName);
|
|
|
|
try
|
|
{
|
|
Client.RenameFile(AdaptPath(Client, path), AdaptPath(Client, path.Parent + newName));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_exHandler(ex);
|
|
|
|
if (_client != null)
|
|
{
|
|
_client.Dispose();
|
|
_client = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>Create Directory and return new entry.</summary>
|
|
/// <param name="path">Path of directory.</param>
|
|
/// <returns>Created entry.</returns>
|
|
public override Directory DirectoryCreate(Path path)
|
|
{
|
|
base.DirectoryCreate(path);
|
|
|
|
try
|
|
{
|
|
Client.CreateDirectory(AdaptPath(Client, path));
|
|
|
|
if (Client.Exists(AdaptPath(Client, path)))
|
|
return CreateEntry(path, Client.Get(AdaptPath(Client, path)), false) as Directory;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_exHandler(ex);
|
|
|
|
if (_client != null)
|
|
{
|
|
_client.Dispose();
|
|
_client = null;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>Deletes the specified directory and, if
|
|
/// indicated, any subdirectories in the directory.</summary>
|
|
/// <param name="path">The path of the directory to remove.</param>
|
|
/// <param name="recursive">Set <c>true</c> to remove directories,
|
|
/// subdirectories, and files in path; otherwise <c>false</c>.</param>
|
|
public override void DirectoryDelete(Path path, bool recursive = false)
|
|
{
|
|
base.DirectoryDelete(path, recursive);
|
|
|
|
try
|
|
{
|
|
if (Client.Exists(AdaptPath(Client, path)))
|
|
Client.DeleteDirectory(AdaptPath(Client, path));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_exHandler(ex);
|
|
|
|
if (_client != null)
|
|
{
|
|
_client.Dispose();
|
|
_client = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Opens an existing file for reading.</summary>
|
|
/// <param name="path">The file to be opened for reading.</param>
|
|
/// <returns>A read-only <see cref="Stream" /> on the specified path.</returns>
|
|
public override Stream FileOpenRead(Path path)
|
|
{
|
|
base.FileOpenRead(path);
|
|
|
|
try
|
|
{
|
|
return Client.OpenRead(AdaptPath(Client, path));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_exHandler(ex);
|
|
|
|
if (_client != null)
|
|
{
|
|
_client.Dispose();
|
|
_client = null;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>Opens an existing file for writing.</summary>
|
|
/// <param name="path">The file to be opened for writing.</param>
|
|
/// <param name="append">Append file.</param>
|
|
/// <returns>An unshared <see cref="Stream" /> object on
|
|
/// the specified path with write access.</returns>
|
|
public override Stream FileOpenWrite(Path path, bool append = false)
|
|
{
|
|
base.FileOpenWrite(path, append);
|
|
|
|
try
|
|
{
|
|
return Client.OpenWrite(AdaptPath(Client, path));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_exHandler(ex);
|
|
|
|
if (_client != null)
|
|
{
|
|
_client.Dispose();
|
|
_client = null;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>Creates or overwrites a file in the specified path.</summary>
|
|
/// <param name="path">The path and name of the file to create.</param>
|
|
/// <param name="overwrite">If file exists replace it.</param>
|
|
/// <returns>A <see cref="Stream" /> that provides
|
|
/// write access to the file specified in path.</returns>
|
|
public override File FileTouch(Path path, bool overwrite = false)
|
|
{
|
|
base.FileTouch(path, overwrite);
|
|
|
|
try
|
|
{
|
|
using (var fs = Client.OpenWrite(AdaptPath(Client, path)))
|
|
fs.Close();
|
|
|
|
return new File
|
|
{
|
|
CreationTime = DateTime.Now,
|
|
LastWriteTime = DateTime.Now,
|
|
LastAccessTime = DateTime.Now,
|
|
Size = 0,
|
|
IsReadOnly = false,
|
|
Path = path,
|
|
FileSystem = this,
|
|
Data = null,
|
|
};
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_exHandler(ex);
|
|
|
|
if (_client != null)
|
|
{
|
|
_client.Dispose();
|
|
_client = null;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>Creates or overwrites a file in the specified path.</summary>
|
|
/// <param name="path">The path and name of the file to create.</param>
|
|
/// <param name="overwrite">If file exists replace it.</param>
|
|
/// <returns>A <see cref="Stream" /> that provides
|
|
/// write access to the file specified in path.</returns>
|
|
public override Stream FileCreate(Path path, bool overwrite = false)
|
|
{
|
|
base.FileCreate(path, overwrite);
|
|
|
|
try
|
|
{
|
|
return Client.OpenWrite(AdaptPath(Client, path));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_exHandler(ex);
|
|
|
|
if (_client != null)
|
|
{
|
|
_client.Dispose();
|
|
_client = null;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
/// <summary>Deletes the specified file. An exception is not thrown
|
|
/// if the specified file does not exist.</summary>
|
|
/// <param name="path">The path of the file to be deleted.</param>
|
|
public override void FileDelete(Path path)
|
|
{
|
|
base.FileDelete(path);
|
|
|
|
try
|
|
{
|
|
if (Client.Exists(AdaptPath(Client, path)))
|
|
Client.DeleteFile(AdaptPath(Client, path));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_exHandler(ex);
|
|
|
|
if (_client != null)
|
|
{
|
|
_client.Dispose();
|
|
_client = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |