-1

here is the problem: take a relative folder structure on Windows, package it in a JSON file, and replicate it on a linux distro, say Ubuntu 20.04, using .NET 5.

Here's my (very much simplified) approach:

using System.IO;
using System.Collections.Generic;
using Newtonsoft.Json;
using Internal;

public class FileData
{
   public string Path { get; set; }
   public string FileName { get; set; }
}

public static void Main(string[] args)
{
    var rootPath = Path.GetFullPath(@"C:/tempFolder");

    var files = Directory.GetFiles(rootPath, "*.*", SearchOption.AllDirectories);

    List<FileData> fileData = new List<FileData>();

    foreach (var file in files)
    {
        fileData.Add(new FileData
        {
            FileName = Path.GetFileName(file),
            Path = Path.GetRelativePath(rootPath, Path.GetDirectoryName(file))
        });
    }
    
    Console.WriteLine(JsonConvert.SerializeObject(fileData));
}

Now this outputs something like

[
    {
        "Path":"InnerFolder\\Subfolder",
        "FileName":"TestFile.txt"
    }
]

Writing the JSON to a file and transferring it to linux, and executing something along the lines of:

var rootPath = Path.GetFullPath("/home/user/tempFolder2/");

var fileData = JsonConvert.DeserializeObject<List<FileData>>(dataReadFromFile);

foreach(var file in fileData)
{
   var path = Path.Combine(rootPath, file.Path, file.FileName);
   //...proceed to create file and path
}

The problem is, the path here returns /home/user/tempFolder2/\InnerFolder\Subfolder/TestFile, as a backslash is a valid path character in linux and it does not get parsed by the Path.Combine.

I figured two options here:

  1. Somehow force windows to use the AltPathSeparatorChar ('/')
  2. Go the poor man's way and simply replace all "\\" and '\' occurences with / in relative paths, if the system is not WinNT32.

Now I severely dislike the second option, in case the paths are generated using linux and a backslash is indeed a backslash. I would much prefer all output files have a standard format for the "Path" variables, but I cannot for the life of me figure out, how to force the Path namespace to use forward slashes for paths instead of backslashes in Windows.

Is there a way achieving this using the Path api or will I have to go the if(OS)... way?

EDIT: After a short debate with @Olivier Rogier, I've decided to update the problem, in case someone knows of an integrated solution.

What it boils down to:

var relativePath = Path.GetRelativePath("./folder", "./folder/sf1/sf2");

returns "sf1\sf2" in Windows and "sf1/sf2" in Linux. So, without additional code on the programmer's end, the line of code is non-deterministic, if the code is run on the two different systems.

Running a simple example like the one here

//WRITER
var relativePath = Path.GetRelativePath("./folder", "./folder/sf1/sf2");
File.WriteAllText("transferFile", relativePath);

//READER, on another OS
var relativePath = File.ReadAllText("transferFile");
var combinedPath = Path.Combine("./folder", relativePath);

does not produce the same result in both environments.

1: Writer: WIN, Reader: WIN
Writer output: sf1\\sf2
Combined path in reader: folder\\sf1\\sf2
Dir create result:

-folder
  -sf1
    -sf2
2: Writer: LNX, Reader: WIN
Writer output: sf1/sf2
Combined path in reader: folder\\sf1\\sf2
Dir create result:
-folder
  -sf1
    -sf2
3: Writer: WIN, Reader: LNX
Writer output: sf1\\sf2
Combined path in reader: folder/sf1\\sf2/
Dir create result:
-folder
  -sf1\\sf2

Another example is using the Linux directory path

-folder
  -sf1\stillavalidfolde\rname
Writer: LNX, Reader: WIN
Writer output: sf1\stillavalidfolde\rname
Combined path in reader: folder\\sf1\\stillavalidfolde\\rname
Dir create result:
-folder
  -sf1
    -stillavalidfolde
      -rname

So far, the only viable solution is something along the lines of

 //WRITER
var relativePath = Path.GetRelativePath("./folder", "./folder/sf1/sf2");
File.WriteAllLines("transferFile", relativePath.Split(Path.DirectorySeparatorChar));

//READER, on another OS
var relativePaths = File.ReadAllLines("transferFile");
var combinedPath = Path.Combine("./folder", relativePaths);

This of course means updates to all serializers, serialization code an so on, so they use the split/join method anywhere the path is serialized, and less human readability.

One would expect at least an overload for the Path namespace methods, where one can either specify a directory separation character (forcing the Path.GetFullPath and Path.Combine methods to always use the same format), a RelativePath class, where this is handled internally, or perhaps another way I haven't thought of.

To reiterate, is there a way to extract, store, and read relative pathnames in strings than can be both read and written on Windows and Linux, without converting the string path to an array of directories?

JasonX
  • 503
  • 7
  • 21
  • Does this answer your question? [Cross-platform file name handling in .NET Core](https://stackoverflow.com/questions/38168391/cross-platform-file-name-handling-in-net-core) and [C# char “//” path separator](https://stackoverflow.com/questions/4597572/c-sharp-char-path-separator) and [how to make path strings in code file system independent?](https://stackoverflow.com/questions/28727345/how-to-make-path-strings-in-code-file-system-independent) –  Aug 10 '21 at 08:59
  • @OlivierRogier, nope, I've read those, my question poses a different problem than those answer. My problem does not lie in handling the paths in code in an instance of an app, but in the written transfer file. Windows ALWAYS packages the path (from GetFullPath or .Combine) with double backslashes during serialization (see json sample). Deserialization of the same string on Linux does not convert the backslashes to directory separator chars (as expected), thus making a simple "get path - write to file - open file on other os - parse path" operation oddly complicated for a standard procedure. – JasonX Aug 11 '21 at 08:18
  • @OlivierRogier So to be clear, I am able to work around the problem myself. I am looking for a standard solution to the problem that does not involve writing a whole path parsing API to allow for a relative path to be written (to a file, db, socket...) on Windows and parsed in Linux. Just seemed like something that could be an overload to the Path API out of the box. – JasonX Aug 11 '21 at 08:22
  • I'm not really advanced and involved in using JSON, but it looks like there is a problem with the way you go about things, or not, I don't know. If the problem is converting a separator created on one platform from another, you must use a conversion method, either predefined or create a new one. And in any case never use the dependent-char itself. Perhaps you can use Path.DirectorySeparatorChar and string. Replace or such a thing to check/convert.. –  Aug 11 '21 at 08:28
  • The JSON isn't much of a factor here, writing the data to any stream from Windows produces the same result, i.e. a path with double backslashes, as the Path API always returns a string containing those in Windows. Which is, of course, incompatible with *nix systems. It's just a problem I'd assume had been handled in one of the .NET APIs, but I guess not. At the very least, I'd expect a serializable, transferrable, all supported OS compatible way of storing relative paths, even if all it does in the backend is what Michael wrote below. Guess a pull request is in order. – JasonX Aug 11 '21 at 08:33
  • [1/2] When you serialize a C# string to JSON, JSON doesn't know it's a path: it's just a raw string. And I don't know if NewtonSoft can handle that with an "parameter-mapping" or can register a callback for that to check fields and return a formatted value. So, you need to check/convert/validate/normalize it, before serializing or when using it if it is intended to be cross-platform. –  Aug 11 '21 at 08:47
  • [2/2] Just use a predefined API or write a simple and short method for that. Delphi have this, if I remember correctly. I don't know about C# ... Just replace the path separator twice: the windows one with the current platform one, and the linux one as well: therefore your method will have one line, so to speak Do you see what I mean? Also you can store an array from the path splitted and join it on the target machine... –  Aug 11 '21 at 08:47
  • I understand as much about the string representation, the question was if there was a built-in/commonly used way to the check/convert/validate/normalize part of it, so I don't reinvent the wheel. Replacing the parameters is out of the question, unless I do an OS case switch (like described in the question). The reason being that \\ to / is a valid conversion on windows, whilst a path may contain \\ in unix, causing path corruption upon replacing characters. It's just a matter of covering all OS combination transfer options without loss of data. For now, the array method is the best proposed. – JasonX Aug 11 '21 at 10:27
  • The problem is that to tell Newtonsoft.Json to process a special string property in a special manner it requires this library offer such a functionnality to format it like done using WinForms controls related events and I don't know if it has that... to say: "Hey! This field is a file system path, process it that way! If you know to do that, do that, please! Otherwise here my event handler method to do that!". Else you need to process the string before and/or after serializing and deserializing. –  Aug 11 '21 at 10:46
  • It's not strictly a JSON problem, I just used it in the example as I was dealing with it at the time. It's more of a System.IO.Path problem, as it spits out OS specific string representations with no overloads to override that. Any form of string data storage suffers from the same problem. The path "./folder/subfolder" is normalized to "folder\\subfolder" in Windows, and translates to a filename on Linux. On Linux, the same conversion produces "folder/subfolder", making the Path API non cross-platform, as a few round trips between apps on different OS-s mangles data beyond recognition. – JasonX Aug 12 '21 at 07:34
  • Does this answer your question? [How should I handle windows/Linux paths in c#](https://stackoverflow.com/questions/5758633/how-should-i-handle-windows-linux-paths-in-c-sharp) –  Aug 12 '21 at 07:45
  • [Managing and handling the filesystem using .NET Core](https://dev.to/packtpartner/managing-and-handling-the-filesystem-using-net-core-ff8) • [Application Portability](https://www.mono-project.com/docs/getting-started/application-portability/) –  Aug 12 '21 at 07:46

1 Answers1

3

To be OS independent, I would save the relative path as an array in your JSON by using the Path.DirectorySeparatorChar property:

// Example: notepad.exe relative to C:\
var pathParts = Path.GetRelativePath(@"C:\", Path.GetDirectoryName(@"C:\Windows\System32\notepad.exe"))
    .Split(Path.DirectorySeparatorChar);

With Path.Combine you can restore your OS dependent part:

var restoredPath = Path.Combine(pathParts);
Michael
  • 1,166
  • 5
  • 4
  • I've though about that, it just seems like a lot of hassle (relatively) to transfer a path. It just might be the only reliable way though. – JasonX Aug 11 '21 at 08:19