Work Backwards Through URL to Find the First Existing Folder - c#

We have written an API that helps internal users upload and download from SharePoint Online via Excel and other tools. The code is all in C# and we have a 'Browser' UI that allows users to navigate to sites/folder/files by host etc.
We've had some feedback that if a user supplies an invalid folder request, can we work backwords until we find the first valid child folder of the Documents root.
So, as an example, a user provides the URL:
https://example.sharepoint.com/sites/client_XXXXXX_Description/Documents/AdditionalDocuments/CalculationMaterials/CalculationLogging/Job99/
Where both CalculationLogging and Job99 doesn't exist. What I need to do is, starting at Job99, work backwards each level until I find a valid folder, i.e. https://example.sharepoint.com/sites/client_XXXXXX_Description/Documents/AdditionalDocuments/CalculationMaterials/
BUT if its get as far back as Documents, then stop, and tell user they have provided an invalid path.
Here's my code though it feels a bit 'clunky'!
// Define new object
var gc = new GraphClient();
// Check to see if the item exists
var exists = await gc.CheckRemoteItemExistsAsync(model.Url);
// Based on the results of the request
switch (exists)
{
// The path is a valid file
case "file":
// Store the actual filename from the path
model.FileName = model.Url.Substring(model.Url.LastIndexOf('/') + 1);
// remove the filename from the URL (as we don't use it)
model.Url = model.Url.Substring(0, model.Url.LastIndexOf('/') + 1);
break;
// The path is a valid folder
case "folder":
// Append a trailing / if not present
if (!model.Url.EndsWith("/")) model.Url += "/";
break;
case null:
// Store the first parent folder as we know the existing child doesn't exist
var subFolder = model.Url.Substring(0, model.Url.LastIndexOf('/') + 1);
var folderLoops = subFolder.Split('/').Length;
// ** Where the path starts with http, deduct 2 from the length **
if (subFolder.Split('/')[0].StartsWith("http"))
{
folderLoops -= 2;
}
// loop through each folder until you find one that is valid
for (var i = folderLoops; i > 1; i--)
{
// If the parent folder is Documents
if (subFolder.Split('/')[i] == "Documents")
{
// Tell user path is invalid
MessageBox.Show(
string.Format(Resources.PathDoesNotExistsWarningMessage, $"{model.Url}"),
Resources.PathDoesNotExistsWarningMessageCaption,
MessageBoxButtons.OK);
// exit the loop
break;
}
// Check to see if the item exists
exists = await gc.CheckRemoteItemExistsAsync(subFolder);
// Based on the results of the request
switch (exists)
{
// The path is a valid file
case "file":
case "folder":
break;
case null:
// Get the parent folder including the trailing /
subFolder = subFolder.Substring(0,
subFolder.Substring(0, subFolder.Length - 1).LastIndexOf('/') + 1);
continue;
}
}
break;
}
The ** comment (line 4) is to explain I'm deducting 2 as when the path starts with https://, the split function will return https as item 0, and an empty string as item 1.
FYI The function CheckRemoteItemExistsAsync is using Graph API to check whether the path contains a valid DriveItem or not.

Try following :
string input = "example.sharepoint.com/sites/client_XXXXXX_Description/Documents/AdditionalDocuments/CalculationMaterials/CalculationLogging/Job99";
string[] splitURL = input.Split(new char[] {'/'});
for(int i = splitURL.Length; i > 0; i--)
{
string[] URL = splitURL.Take(i).ToArray();
Console.WriteLine("https://" + string.Join("/",URL));
}
Console.ReadLine();

Related

How to get file information of list of files using C# and SMBLibrary

I am trying to get file information such as file name, last modified date/time and file size etc. from a list of Excel spreadsheet from a shared folder. I used C# and Tal Aloni's SMBLibary 1.4.8 to do that.
I need help to get the file information out from 'List' class fileList. Below is the program snippet working so far. I am able to logon the SMB server and connected to shared folder 'MPDS. The folder I working on have 9 Excel spreadsheet. When I printed out the 'fileList' element count, it returned 9 (the log statement: log.LogInformation("Number of fileList element: " + fileList.Count.ToString());).
I appreciated anyone giving me help or tips.
Note: I replaced the IP and the credential of the code snippet with a description.
var client = new SMB2Client();
bool isConnected = client.Connect(IPAddress.Parse("<IP address>"), SMBTransportType.DirectTCPTransport);
if (isConnected)
{
log.LogInformation("Connected to SMB");
NTStatus status = client.Login("<domain>", "<user ID>", "<password>");
if (status == NTStatus.STATUS_SUCCESS)
{
log.LogInformation("Logged in as username");
string filePath = #"Purchasing Admin\D365\Load Templates\Test Templates\" + cFileName + ".txt";
ISMBFileStore fileStore = client.TreeConnect("MPDS", out status);
status = fileStore.CreateFile(out object directoryHandle, out FileStatus fileStatus, #"Purchasing Admin\D365\Load Templates\Test Templates", AccessMask.GENERIC_READ, FileAttributes.Directory, ShareAccess.Read | ShareAccess.Write, CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE, null);
if (status == NTStatus.STATUS_SUCCESS)
{
status = fileStore.QueryDirectory(out List<QueryDirectoryFileInformation> fileList, directoryHandle, "*.xlsx", FileInformationClass.FileDirectoryInformation);
log.LogInformation("Number of fileList element: " + fileList.Count.ToString());
status = fileStore.CloseFile(directoryHandle);
}
}
}
You are almost there with what you have in your provided code sample. You just need to inspect the files returned in your fileList, but need to cast to the appropriate FileDirectoryInformation type to get the file attributes you are seeking. For example:
if (status == NTStatus.STATUS_SUCCESS)
{
List<QueryDirectoryFileInformation> fileList;
status = fileStore.QueryDirectory(out fileList, directoryHandle, "*", FileInformationClass.FileDirectoryInformation);
status = fileStore.CloseFile(directoryHandle);
//
// Here we are casting each item in the fileList to a FileDirectoryInformation
// type that holds the attributes you are looking for.
//
foreach(FileDirectoryInformation file in fileList)
{
Console.WriteLine($"Filename: {file.FileName}");
Console.WriteLine($"File Size: {file.AllocationSize / 1024}KB");
Console.WriteLine($"Created Date: {file.CreationTime.ToString("f")}");
Console.WriteLine($"Last Modified Date: {file.LastWriteTime.ToString("f")}");
Console.WriteLine();
Console.WriteLine();
}
}
Note that I am just assuming that the file size you are looking for is in KB, but there are more advanced techniques to produce a better human readable size (you can check out libraries like Humanizer etc.

Check directory structure

I'm trying to check whether a directory is in a format of
Ex:
This should be valid:
D:\TESTDIR\FOLDER1\FOLDER2\Somerandomfiles
This is not valid:
D:\TESTDIR\FOLDER1\Somerandomfiles
Basically I need to make sure that for every folder there should be a subfolder inside. note that a single folder may contain multiple subfolder. Then the subfolder will contain the files such as text files/pdf files etc.
//not sure how/where to place to check if folder has a subfolder
var _dir = Directory.GetDirectories(#"D:\TESTDIR");
foreach(var _folder1 in _dir)
{
// check if contains folder2? if not
// MessageBox.Show(folder1name);
var _folder2 = Directory.GetDirectories(_folder1);
foreach (var _path in _folder2)
{
// do something
}
}
You almost got it right, I just modified your code a little:
var dir = Directory.GetDirectories(#"D:\TESTDIR");
foreach(var folder1 in dir)
{
var folder2 = Directory.GetDirectories(folder1);
// If no directories are found in that subfolder, skip it.
// Basically it would work the same without if, as loop
// would not run even one iteration, but it's more readable.
if(folder2.Count == 0) continue;
foreach (var path in folder2)
{
var files = Directory.GetFiles(path);
// Same as above, check if there are any files.
if(files.Count == 0) break;
// Do some work
}
}
Here's how I would start this:
var query =
from dir in Directory.GetDirectories(#"D:\TESTDIR")
select new
{
dir,
valid =
Directory.GetDirectories(dir).Any()
&& Directory.GetDirectories(dir).All(child => Directory.GetFiles(child).Any())
};
That's going to check each of the directories in #"D:\TESTDIR" and ensure each has at least one subfolder and that each subfolder has at least one file.

C# FolderBrowserDialog - how to include shared folders

I create a FolderBrowserDialog as follows (only an excerpt -not complete code):
string tempSetWorkingPath = null;
try
{
FolderBrowserDialog folderDlg = new System.Windows.Forms.FolderBrowserDialog();
folderDlg.ShowNewFolderButton = true;
folderDlg.Description = "Selected your working folder. This is where your PDF files will be saved.";
folderDlg.RootFolder = Environment.SpecialFolder.MyComputer;
folderDlg.SelectedPath = (Convert.ToString(WorkingPath).Trim().Length == 0) ? ((int)Environment.SpecialFolder.MyComputer).ToString() : WorkingPath;
if (folderDlg.ShowDialog() == DialogResult.OK)
{
tempSetWorkingPath = folderDlg.SelectedPath;
}
else
{
tempSetWorkingPath = "";
}
}
...
The code works well, except the only folders that are showing are the local folders. Users have DropBox and OneDrive shared folders on their systems and to select one of those directories, the user needs to cycle through the windows user directories and select the folder from there. On some systems I have seen over the last few months, I've seen the DropBox and OneDrive directories appear below the DeskTop directory ... but I have not, despite hours of searching - found a way to achive that.
How can I achieve that?
MTIA
DWE
Given I have observed a large number of queries posted here and elsewhere regarding the inclusion of the directories, including shared directories and given the response by # Mailosz, it seems that the root folder property of the folder dialog holds the key - it Gets or sets the root folder where the browsing starts from and thats what my code was missing.
The full code to the function referred to in my question appears below, in the event it assists someone else.
/// <summary>
/// presents the user with a folder dialog
/// Returns a full qualified directory chosen by the user
/// </summary>
/// <param name="WorkingPath">if a fully qualified directory name is provided, then the folder structure in the folder dialog will open to the directory selected</param>
/// <returns>Returns a full qualified directory chosen by the user or if no directory was chosen, an empty string</returns>
public string SetWorkingPath(string WorkingPath)
{
string tempSetWorkingPath = null;
try
{
FolderBrowserDialog folderDlg = new System.Windows.Forms.FolderBrowserDialog();
// check our proposed working path and if its valid
if((!string.IsNullOrEmpty(WorkingPath) && (WorkingPath != null)))
{
if (!Directory.Exists(WorkingPath))
WorkingPath = string.Empty;
}
else // if we are empty or null set us to empty
{
WorkingPath = string.Empty;
}
folderDlg.ShowNewFolderButton = true;
folderDlg.Description = "Please select your working folder. This is where your PDF files will be saved.";
folderDlg.RootFolder = Environment.SpecialFolder.Desktop;//.MyComputer;
folderDlg.SelectedPath = (Convert.ToString(WorkingPath).Trim().Length == 0) ? ((int)Environment.SpecialFolder.MyComputer).ToString() : WorkingPath;
if (folderDlg.ShowDialog() == DialogResult.OK)
{
// make sure we have a backslash on the end of our directory string
tempSetWorkingPath = PathAddBackslash(folderDlg.SelectedPath);
}
else
{
// return an empty string
tempSetWorkingPath = string.Empty;
}
}
catch (Exception ex)
{
tempSetWorkingPath = string.Empty;
throw (ex);
}
return tempSetWorkingPath;
}
public string PathAddBackslash(string path)
{
// They're always one character but EndsWith is shorter than
// array style access to last path character. Change this
// if performance are a (measured) issue.
string separator1 = Path.DirectorySeparatorChar.ToString();
string separator2 = Path.AltDirectorySeparatorChar.ToString();
// Trailing white spaces are always ignored but folders may have
// leading spaces. It's unusual but it may happen. If it's an issue
// then just replace TrimEnd() with Trim(). Tnx Paul Groke to point this out.
path = path.TrimEnd();
// Argument is always a directory name then if there is one
// of allowed separators then I have nothing to do.
if (path.EndsWith(separator1) || path.EndsWith(separator2))
return path;
// If there is the "alt" separator then I add a trailing one.
// Note that URI format (file://drive:\path\filename.ext) is
// not supported in most .NET I/O functions then we don't support it
// here too. If you have to then simply revert this check:
// if (path.Contains(separator1))
// return path + separator1;
//
// return path + separator2;
if (path.Contains(separator2))
return path + separator2;
// If there is not an "alt" separator I add a "normal" one.
// It means path may be with normal one or it has not any separator
// (for example if it's just a directory name). In this case I
// default to normal as users expect.
return path + separator1;
}

How to build a list of directories from args that may contain drive letters and/or wildcards

I want to pass a list of directories to my C# console application as arguments which may be relative paths based on the current working directory, or contain drive letters and/or wildcards. E.g.:
myApp.exe someDir another\dir dirs\with\wild*cards* ..\..\a\relative\dir C:\an\absolute\dir\with\wild*cards*
So far I've come up with this:
static void Main(string[] args)
{
List<string> directories = new List<string>();
foreach (string arg in args)
{
if (Directory.Exists(arg))
{
// 'arg' by itself is a valid directory, and must not contain any wildcards
// just add it to 'directories'
directories.Add(arg);
}
else
{
// 'arg' must either be a non-existant directory or invalid directory name,
// or else it contains wildcard(s). Find all matching directory names, starting
// at the current directory (assuming 'arg' might be a relative path), and add
// all matching directory names to 'directories'.
string[] dirs = Directory.GetDirectories(Directory.GetCurrentDirectory(), arg, SearchOption.TopDirectoryOnly);
directories.AddRange(dirs);
}
}
Console.WriteLine("Full list of directories specified by the command line args:");
foreach (string dir in directories)
{
Console.WriteLine(" " + dir);
}
// Now go do what I want to do for each of these directories...
}
This works great for someDir, another\dir, and dirs\with\wild*cards*, but won't work for ..\..\a\relative\dir or C:\an\abolute\dir\with\wild*cards*. For the relative dir, Directory.GetDirectories() throws a System.ArgumentException saying "Search pattern cannot contain '..' to move up directories and can be contained only internally in file/directory names, as in 'a..b'." For the drive-letter-based directory, it throws a System.ArgumentException saying "Second path fragment must not be a drive or UNC name."
How can I handle the ".." and drive letters? This has to be a solved problem, but I can't find examples of such for C#.
"Bonus question": My above code also doesn't handle a\path\with\wild*cards*\in\anything\other\than\the\top\directory. Any easy way to handle that too?
Couple of observations:
1) To check if path absolute or relative - you can use Path.IsRooted()
2) To resolve path with ".." to absolute path (be it relative or absolute) you can use:
path = new Uri(Path.Combine(Directory.GetCurrentDirectory(), path)).AbsolutePath;
Routing it though Uri expands those dots. Path.GetFullPath will fail in case of wildcards, but Uri will not.
3) To check if path contains wildcards you can just do path.Contains("*") || path.Contains("?") since both of those characters are not valid path chars, so cannot be present in a context other than being wildcards.
4) To resolve wildcard in absolute path (and match your "bonus" requirement) you need to find out first directory which does not contain wildcard. So you basically need to split path into to parts - before first wildcard and after first wildcard. For example:
C:\an\abolute\dir\with\wild*cards*
Path before wildcard is C:\an\abolute\dir\with, after (and including) wildcard: wild*cards*.
C:\an\abolu*e\dir\with\wild*cards*
Path before first wildcard: C:\an, after: abolu*e\dir\with\wild*cards*. There are different ways to do that of course, easiest I think is regex:
#"[\\/](?=[^\\/]*[\*?])"
It basically matches directory separator, but only if it is followed by 0 or more charactres which are NOT directory separators, then followed by wildcard symbol.
Combining this all together we have:
static IEnumerable<string> ResolveDirectories(string path) {
if (path.Contains("*") || path.Contains("?")) {
// we have a wildcard,
// resolve it to absolute path if necessary
if (!Path.IsPathRooted(path))
path = Path.Combine(Directory.GetCurrentDirectory(), path);
// resolve .. stuff if any
path = new Uri(Path.Combine(Directory.GetCurrentDirectory(), path)).AbsolutePath;
// split with regex above, only on first match (2 parts)
var parts = new Regex(#"[\\/](?=[^\\/]*[\*?])").Split(path, 2);
var searchRoot = parts[0];
var searchPatterns = parts[1].Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
foreach (var dir in ResolveWildcards(searchRoot, searchPatterns))
yield return dir;
}
else {
// this should work with "../.." type of paths too, will resolve relative to current directory
// so no processing is necessary
if (Directory.Exists(path)) {
yield return path;
}
else {
// invalid directory?
}
}
}
static IEnumerable<string> ResolveWildcards(string searchRoot, string [] searchPatterns) {
// if we have path C:\an\abolu*e\dir\with\wild*cards*
// search root is C:\an
// and patterns are: [abolu*e, dir, with, wild*cards*]
if (Directory.Exists(searchRoot)) {
// use next pattern to search in a search root
var next = searchPatterns[0];
// leave the rest for recursion
var rest = searchPatterns.Skip(1).ToArray();
foreach (var dir in Directory.EnumerateDirectories(searchRoot, next, SearchOption.AllDirectories)) {
// if nothing left (last pattern) - return it
if (rest.Length == 0)
yield return dir;
else {
// otherwise search with rest patterns in freshly found directory
foreach (var sub in ResolveWildcards(dir, rest))
yield return sub;
}
}
}
}
This is not properly tested, so take care.
'Evk' posted his/her answer a few minutes before I was about to post this, so I have not evaluated Evk's answer.
After finding this question+answer which might have worked fine but I didn't test it, I stumbled across Michael Ganss's Glob.cs NuGet package, and the results are very nice:
// Requires Glob.cs, (c) 2013 Michael Ganss, downloaded via the NuGet package manager:
// https://www.nuget.org/packages/Glob.cs
// (In Visual Studio, go to Tools->NuGet Package Manager->Package Manager Console.)
// PM> Install-Package Glob.cs -Version 1.3.0
using Glob;
namespace StackOverflow_cs
{
class Program
{
static void Main(string[] args)
{
List<string> directories = new List<string>();
foreach (string arg in args)
{
var dirs = Glob.Glob.Expand(arg, true, true);
foreach (var dir in dirs)
{
directories.Add(dir.FullName);
}
}
Console.WriteLine("Full list of directories specified by the command line args:");
foreach (string dir in directories)
{
Console.WriteLine(" " + dir);
}
// Now go do what I want to do for each of these directories......
}
}
}
But I don't know why I have to say "Glob.Glob.Expand()" instead of simply "Glob.Expand()". Anyway, it works beautifully.

Error: File Path is Too Long

i am trying to use the various file functions in C# like File.GetLastWriteTime, copy command on the file placed at the path greater than maximum allowed path on windows 7 i.e 260. Its giving me an error on long path name. On MSDN support i they have asked to use the \\?\ before the path. I did the same but still i got the same error, it seems it doesn't make any change. Below is my code. Please let me know if i am using it correct or i need to add any thing:
These all lib i am using as the code is having other things also:
the below is the respective code:
filesToBeCopied = Directory.GetFiles(path,"*",SearchOption.AllDirectories);
for (int j = 0; j < filesToBeCopied.Length; j++)
{
try
{
String filepath = #"\\?\" + filesToBeCopied[j];
File.GetLastWriteTime(filepath);
}
catch (Exception ex)
{
MessageBox.Show("Error Inside the single file iteration for the path:" +
filesToBeCopied[j] + " . The exception is :" + ex.Message);
}
}
where as path is the path to the folder at windows machine starting with drive letter. for ex.: d:\abc\bcd\cd\cdc\dc\..........
Here's a solution for at least the copying portion of your request (thank you pinvoke.net):
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
static extern bool CopyFile(string lpExistingFileName, string lpNewFileName, bool bFailIfExists);
And then to actually copy your file:
// Don't forget the '\\?\' for long paths
string reallyLongPath = #"\\?\d:\abc\bcd\cd\cdc\dc\..........";
string destination = #"C:\some\other\path\filename.txt";
CopyFile(reallyLongPath , destination, false);
As far as I know, you can't access a file directly if its path is too long (by directly, I mean using the methods of File, by creating a FileInfo via the constructor, or by using Directory.GetFiles(string fileName).
The only way I've found that will let you access such a file is to access a directory somewhere in the path before it gets too long, and then programatically walk down the tree until you get to your file, as seen here.
I've taken my code from there and modified it a little to return a FileInfo object for a file with a path that is "too long". Using this code, you can access the necessary properties on the returned FileInfo object (like LastWriteTime). It still has some limitations though, like the inability to use functions like CopyTo() or OpenText().
// Only call GetFileWithLongPath() if the path is too long
// ... otherwise, new FileInfo() is sufficient
private static FileInfo GetFile(string path)
{
if (path.Length >= MAX_FILE_PATH)
{
return GetFileWithLongPath(path);
}
else return new FileInfo(path);
}
static int MAX_FILE_PATH = 260;
static int MAX_DIR_PATH = 248;
private static FileInfo GetFileWithLongPath(string path)
{
string[] subpaths = path.Split('\\');
StringBuilder sbNewPath = new StringBuilder(subpaths[0]);
// Build longest sub-path that is less than MAX_PATH characters
for (int i = 1; i < subpaths.Length; i++)
{
if (sbNewPath.Length + subpaths[i].Length >= MAX_DIR_PATH)
{
subpaths = subpaths.Skip(i).ToArray();
break;
}
sbNewPath.Append("\\" + subpaths[i]);
}
DirectoryInfo dir = new DirectoryInfo(sbNewPath.ToString());
bool foundMatch = dir.Exists;
if (foundMatch)
{
// Make sure that all of the subdirectories in our path exist.
// Skip the last entry in subpaths, since it is our filename.
// If we try to specify the path in dir.GetDirectories(),
// We get a max path length error.
int i = 0;
while (i < subpaths.Length - 1 && foundMatch)
{
foundMatch = false;
foreach (DirectoryInfo subDir in dir.GetDirectories())
{
if (subDir.Name == subpaths[i])
{
// Move on to the next subDirectory
dir = subDir;
foundMatch = true;
break;
}
}
i++;
}
if (foundMatch)
{
// Now that we've gone through all of the subpaths, see if our file exists.
// Once again, If we try to specify the path in dir.GetFiles(),
// we get a max path length error.
foreach (FileInfo fi in dir.GetFiles())
{
if (fi.Name == subpaths[subpaths.Length - 1])
{
return fi;
}
}
}
}
// If we didn't find a match, return null;
return null;
}
Now that you've seen that, go rinse your eyes and shorten your paths.
try with this code
var path = Path.Combine(#"\\?\", filesToBeCopied[j]); //don't forget extension
"\?\" prefix to a path string tells the Windows APIs to disable all string parsing and to send the string that follows it straight to the file system.
Important : Not all file I/O APIs support "\?\", you should look at the reference topic for each API
http://www.codinghorror.com/blog/2006/11/filesystem-paths-how-long-is-too-long.html
I recently imported some source code for a customer that exceeded the maximum path limit of 256 characters.
The path you pasted was 285 characters long.
As you noted in your comment, MSDN's link here (http://msdn.microsoft.com/en-us/library/aa365247%28VS.85%29.aspx#maximum%5Fpath%5Flength) explains this length in greater detail:
In the Windows API (with some exceptions discussed in the following paragraphs), the maximum length for a path is MAX_PATH, which is defined as 260 characters. A local path is structured in the following order: drive letter, colon, backslash, name components separated by backslashes, and a terminating null character. For example, the maximum path on drive D is "D:\some 256-character path string" where "" represents the invisible terminating null character for the current system codepage. (The characters < > are used here for visual clarity and cannot be part of a valid path string.)
With respect to the \\?\ functionality:
Many but not all file I/O APIs support "\?\"; you should look at the reference topic for each API to be sure.

Categories

Resources