<commit_before>﻿//  Copyright (C) 2015, The Duplicati Team
//  http://www.duplicati.com, <EMAIL>
//
//  This library is free software; you can redistribute it and/or modify
//  it under the terms of the GNU Lesser General Public License as
//  published by the Free Software Foundation; either version 2.1 of the
//  License, or (at your option) any later version.
//
//  This library is distributed in the hope that it will be useful, but
//  WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
//  Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public
//  License along with this library; if not, write to the Free Software
//  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;

namespace Duplicati.Server.WebServer.RESTMethods
{
    public class Filesystem : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented
    {
        public void GET(string key, RequestInfo info)
        {
            var parts = (key ?? "").Split(new char[] { '/' });
            var path = Duplicati.Library.Utility.Uri.UrlDecode((parts.Length == 2 ? parts.FirstOrDefault() : key ?? ""));
            var command = parts.Length == 2 ? parts.Last() : null;
            if (string.IsNullOrEmpty(path))
                path = info.Request.QueryString["path"].Value;
            
            Process(command, path, info);
        }

        private void Process(string command, string path, RequestInfo info)
        {
            if (string.IsNullOrEmpty(path))
            {
                info.ReportClientError("No path parameter was found");
                return;
            }

            bool skipFiles = Library.Utility.Utility.ParseBool(info.Request.QueryString["onlyfolders"].Value, false);
            bool showHidden = Library.Utility.Utility.ParseBool(info.Request.QueryString["showhidden"].Value, false);

            string specialpath = null;
            string specialtoken = null;

            if (path.StartsWith("%"))
            {
                var ix = path.IndexOf("%", 1);
                if (ix > 0)
                {
                    var tk = path.Substring(0, ix + 1);
                    var node = SpecialFolders.Nodes.Where(x => x.id.Equals(tk, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
                    if (node != null)
                    {
                        specialpath = node.resolvedpath;
                        specialtoken = node.id;
                    }
                }
            }

            path = SpecialFolders.ExpandEnvironmentVariables(path);

            if (Duplicati.Library.Utility.Utility.IsClientLinux && !path.StartsWith("/"))
            {
                info.ReportClientError("The path parameter must start with a forward-slash");
                return;
            }

            if (!string.IsNullOrWhiteSpace(command))
            {
                if ("validate".Equals(command, StringComparison.InvariantCultureIgnoreCase))
                {
                    try
                    {
                        if (System.IO.Path.IsPathRooted(path) && (System.IO.Directory.Exists(path) || System.IO.File.Exists(path)))
                        {
                            info.OutputOK();
                            return;
                        }
                    }
                    catch
                    {
                    }

                    info.ReportServerError("File or folder not found");
                    return;
                }
                else
                {
                    info.ReportClientError(string.Format("No such operation found: {0}", command));
                    return;
                }
            }

            try
            {
                if (path != "" && path != "/")
                    path = Duplicati.Library.Utility.Utility.AppendDirSeparator(path);

                IEnumerable<Serializable.TreeNode> res;

                if (!Library.Utility.Utility.IsClientLinux && (path.Equals("/") || path.Equals("")))
                {
                    res = 
                        from di in System.IO.DriveInfo.GetDrives()
                            where di.DriveType == DriveType.Fixed || di.DriveType == DriveType.Network || di.DriveType == DriveType.Removable
                        select new Serializable.TreeNode()
                    {
                        id = di.RootDirectory.FullName,
                        text = 
                            (
                                string.IsNullOrWhiteSpace(di.VolumeLabel) ? 
                                di.RootDirectory.FullName.Replace('\\', ' ') : 
                                di.VolumeLabel + " - " + di.RootDirectory.FullName.Replace('\\', ' ')
                            ) + "(" + di.DriveType + ")",
                        iconCls = "x-tree-icon-drive"
                    };
                }
                else
                {
                    res = ListFolderAsNodes(path, skipFiles, showHidden);                        
                }

                if ((path.Equals("/") || path.Equals("")) && specialtoken == null) 
                {
                    // Prepend special folders
                    res = SpecialFolders.Nodes.Union(res);
                }

                if (specialtoken != null)
                {
                    res = res.Select(x => { 
                        x.resolvedpath = x.id;
                        x.id = specialtoken + x.id.Substring(specialpath.Length);
                        return x; 
                    });
                }

                info.OutputOK(res);
            }
            catch (Exception ex)
            {
                info.ReportClientError("Failed to process the path: " + ex.Message);
            }
        }

        private static IEnumerable<Serializable.TreeNode> ListFolderAsNodes(string entrypath, bool skipFiles, bool showHidden)
        {
            //Helper function for finding out if a folder has sub elements
            Func<string, bool> hasSubElements = (p) => skipFiles ? Directory.EnumerateDirectories(p).Any() : Directory.EnumerateFileSystemEntries(p).Any();

            //Helper function for dealing with exceptions when accessing off-limits folders
            Func<string, bool> isEmptyFolder = (p) =>
            {
                try { return !hasSubElements(p); }
                catch { }
                return true;
            };

            //Helper function for dealing with exceptions when accessing off-limits folders
            Func<string, bool> canAccess = (p) =>
            {
                try { hasSubElements(p); return true; }
                catch { }
                return false;
            }; 

            var systemIO = Library.Utility.Utility.IsClientLinux
                ? (Duplicati.Library.Snapshots.ISystemIO)new Duplicati.Library.Snapshots.SystemIOLinux() 
                : (Duplicati.Library.Snapshots.ISystemIO)new Duplicati.Library.Snapshots.SystemIOWindows();

            foreach (var s in System.IO.Directory.EnumerateFileSystemEntries(entrypath))
            {
                Serializable.TreeNode tn = null;
                try
                {
                    var attr = systemIO.GetFileAttributes(s);
                    var isSymlink = (attr & FileAttributes.ReparsePoint) != 0;
                    var isFolder = (attr & FileAttributes.Directory) != 0;
                    var isFile = !isFolder;
                    var isHidden = (attr & FileAttributes.Hidden) != 0;<commit_msg>Fixed multiple issues regarding listing folders and drives<commit_after>﻿//  Copyright (C) 2015, The Duplicati Team
//  http://www.duplicati.com, <EMAIL>
//
//  This library is free software; you can redistribute it and/or modify
//  it under the terms of the GNU Lesser General Public License as
//  published by the Free Software Foundation; either version 2.1 of the
//  License, or (at your option) any later version.
//
//  This library is distributed in the hope that it will be useful, but
//  WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
//  Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public
//  License along with this library; if not, write to the Free Software
//  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;

namespace Duplicati.Server.WebServer.RESTMethods
{
    public class Filesystem : IRESTMethodGET, IRESTMethodPOST, IRESTMethodDocumented
    {
        public void GET(string key, RequestInfo info)
        {
            var parts = (key ?? "").Split(new char[] { '/' });
            var path = Duplicati.Library.Utility.Uri.UrlDecode((parts.Length == 2 ? parts.FirstOrDefault() : key ?? ""));
            var command = parts.Length == 2 ? parts.Last() : null;
            if (string.IsNullOrEmpty(path))
                path = info.Request.QueryString["path"].Value;

            Process(command, path, info);
        }

        private void Process(string command, string path, RequestInfo info)
        {
            if (string.IsNullOrEmpty(path))
            {
                info.ReportClientError("No path parameter was found");
                return;
            }

            bool skipFiles = Library.Utility.Utility.ParseBool(info.Request.QueryString["onlyfolders"].Value, false);
            bool showHidden = Library.Utility.Utility.ParseBool(info.Request.QueryString["showhidden"].Value, false);

            string specialpath = null;
            string specialtoken = null;

            if (path.StartsWith("%"))
            {
                var ix = path.IndexOf("%", 1);
                if (ix > 0)
                {
                    var tk = path.Substring(0, ix + 1);
                    var node = SpecialFolders.Nodes.Where(x => x.id.Equals(tk, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
                    if (node != null)
                    {
                        specialpath = node.resolvedpath;
                        specialtoken = node.id;
                    }
                }
            }

            path = SpecialFolders.ExpandEnvironmentVariables(path);

            if (Duplicati.Library.Utility.Utility.IsClientLinux && !path.StartsWith("/"))
            {
                info.ReportClientError("The path parameter must start with a forward-slash");
                return;
            }

            if (!string.IsNullOrWhiteSpace(command))
            {
                if ("validate".Equals(command, StringComparison.InvariantCultureIgnoreCase))
                {
                    try
                    {
                        if (System.IO.Path.IsPathRooted(path) && (System.IO.Directory.Exists(path) || System.IO.File.Exists(path)))
                        {
                            info.OutputOK();
                            return;
                        }
                    }
                    catch
                    {
                    }

                    info.ReportServerError("File or folder not found");
                    return;
                }
                else
                {
                    info.ReportClientError(string.Format("No such operation found: {0}", command));
                    return;
                }
            }

            try
            {
                if (path != "" && path != "/")
                    path = Duplicati.Library.Utility.Utility.AppendDirSeparator(path);

                IEnumerable<Serializable.TreeNode> res;

                if (!Library.Utility.Utility.IsClientLinux && (path.Equals("/") || path.Equals("")))
                {
                    res = DriveInfo.GetDrives()
		                    .Where(di =>
								(di.DriveType == DriveType.Fixed || di.DriveType == DriveType.Network || di.DriveType == DriveType.Removable)
								&& di.IsReady // Only try to create TreeNode entries for drives who were ready 'now'
							)
		                    .Select(TryCreateTreeNodeForDrive) // This will try to create a TreeNode for selected drives
							.Where(tn => tn != null); // This filters out such entries that could not be created
                }
                else
                {
                    res = ListFolderAsNodes(path, skipFiles, showHidden);
                }

                if ((path.Equals("/") || path.Equals("")) && specialtoken == null)
                {
                    // Prepend special folders
                    res = SpecialFolders.Nodes.Union(res);
                }

                if (specialtoken != null)
                {
                    res = res.Select(x => {
                        x.resolvedpath = x.id;
                        x.id = specialtoken + x.id.Substring(specialpath.Length);
                        return x;
                    });
                }

				// We have to resolve the query before giving it to OutputOK
				// If we do not do this, and the query throws an exception when OutputOK resolves it,
				// the exception would not be handled properly
	            res = res.ToList();

				info.OutputOK(res);
            }
            catch (Exception ex)
            {
                info.ReportClientError("Failed to process the path: " + ex.Message);
            }
        }

		/// <summary>
		/// Try to create a new TreeNode instance for the given DriveInfo instance.
		///
		/// <remarks>
		/// If an exception occurs during creation (most likely the device became unavailable), a null is returned instead.
		/// </remarks>
		/// </summary>
		/// <param name="driveInfo">DriveInfo to try create a TreeNode for. Cannot be null.</param>
		/// <returns>A new TreeNode instance on success; null if an exception occurred during creation.</returns>
		private static Serializable.TreeNode TryCreateTreeNodeForDrive(DriveInfo driveInfo)
	    {
			if (driveInfo == null) throw new ArgumentNullException("driveInfo");

		    try
		    {
				// Try to create the TreeNode
				// This may still fail as the drive might become unavailable in the meanwhile
				return new Serializable.TreeNode
				{
					id = driveInfo.RootDirectory.FullName,
					text =
					(
						string.IsNullOrWhiteSpace(driveInfo.VolumeLabel)
							? driveInfo.RootDirectory.FullName.Replace('\\', ' ')
							: driveInfo.VolumeLabel + " - " + driveInfo.RootDirectory.FullName.Replace('\\', ' ')
						) + "(" + driveInfo.DriveType + ")",
					iconCls = "x-tree-icon-drive"
				};
			}
		    catch
		    {
				// Drive became unavailable in the meanwhile or another exception occurred
				// Return a null as fall back
				return null;
		    }
	    }

        private static IEnumerable<Serializable.TreeNode> ListFolderAsNodes(string entrypath, bool skipFiles, bool showHidden)
        {
            //Helper function for finding out if a folder has sub elements
            Func<string, bool> hasSubElements = (p) => skipFiles ? Directory.EnumerateDirectories(p).Any() : Directory.EnumerateFileSystemEntries(p).Any();

            //Helper function for dealing with exceptions when accessing off-limits folders
            Func<string, bool> isEmptyFolder = (p) =>
            {
                try { return !hasSubElements(p); }
                catch { }
                return true;
            };

            //Helper function for dealing with exceptions when accessing off-limits folders
            Func<string, bool> canAccess = (p) =>
            {
                try { hasSubElements(p); return true; }
                catch { }
                return false;
            };

            var systemIO = Library.Utility.Utility.IsClientLinux
                ? (Duplicati.Library.Snapshots.ISystemIO)new Duplicati.Library.Snapshots.SystemIOLinux()
                : (Duplicati.Library.Snapshots.ISystemIO)new Duplicati.Library.Snapshots.SystemIOWindows();

            foreach (var s in System.IO.Directory.EnumerateFileSystemEntries(entrypath))
            {
                Serializable.TreeNode tn = null;
                try
                {
                    var attr = systemIO.GetFileAttributes(s);
                    var isSymlink = (attr & FileAttributes.ReparsePoint) != 0;
                    var isFolder = (attr & FileAttributes.Directory) != 0;
                    var isFile = !isFolder;
                    var isHidden = (attr & FileAttributes.Hidden) != 0;
{'fraction_non_alphanumeric': 0.09740647782956854, 'fraction_numerical': 0.0035855145213338113, 'mean_word_length': 1.504864541236342, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 37, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2490116', 'n_tokens_mistral': 4396, 'n_tokens_neox': 3986, 'n_words': 1077}