Как получить относительный путь от абсолютного пути


174

В моих приложениях есть часть, которая отображает путь к файлу, загруженный пользователем через OpenFileDialog. Он занимает слишком много места для отображения всего пути, но я не хочу отображать только имя файла, поскольку это может быть неоднозначным. Поэтому я бы предпочел показать путь к файлу относительно каталога assembly / exe.

Например, сборка находится в, C:\Program Files\Dummy Folder\MyProgramа файл в C:\Program Files\Dummy Folder\MyProgram\Data\datafile1.datто время я хотел бы показать .\Data\datafile1.dat. Если файл находится в C:\Program Files\Dummy Folder\datafile1.dat, то я бы хотел ..\datafile1.dat. Но если файл находится в корневом каталоге или в 1 каталоге ниже корневого, отобразите полный путь.

Какое решение вы бы порекомендовали? Regex?

В основном я хочу отобразить полезную информацию о пути к файлу, не занимая слишком много места на экране.

РЕДАКТИРОВАТЬ: Просто чтобы уточнить немного больше. Цель этого решения - помочь пользователю или мне узнать, какой файл я загружал последним, и примерно из какого каталога он был. Я использую текстовое поле только для чтения, чтобы отобразить путь. В большинстве случаев путь к файлу намного длиннее, чем пространство для отображения текстового поля. Путь должен быть информативным, но не настолько важным, чтобы занимать больше места на экране.

Комментарий Алекса Браула был хорошим, как и Джонатана Леффлера. Функция Win32, предоставляемая DavidK, помогает только в части проблемы, но не в целом, но все равно спасибо. Что касается решения Джеймса Ньютона-Кинга, я попробую его позже, когда буду свободен.


Ответы:


192

.NET Core 2.0 Path.GetRelativePathиспользует это еще.

/// <summary>
/// Creates a relative path from one file or folder to another.
/// </summary>
/// <param name="fromPath">Contains the directory that defines the start of the relative path.</param>
/// <param name="toPath">Contains the path that defines the endpoint of the relative path.</param>
/// <returns>The relative path from the start directory to the end path or <c>toPath</c> if the paths are not related.</returns>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="UriFormatException"></exception>
/// <exception cref="InvalidOperationException"></exception>
public static String MakeRelativePath(String fromPath, String toPath)
{
    if (String.IsNullOrEmpty(fromPath)) throw new ArgumentNullException("fromPath");
    if (String.IsNullOrEmpty(toPath))   throw new ArgumentNullException("toPath");

    Uri fromUri = new Uri(fromPath);
    Uri toUri = new Uri(toPath);

    if (fromUri.Scheme != toUri.Scheme) { return toPath; } // path can't be made relative.

    Uri relativeUri = fromUri.MakeRelativeUri(toUri);
    String relativePath = Uri.UnescapeDataString(relativeUri.ToString());

    if (toUri.Scheme.Equals("file", StringComparison.InvariantCultureIgnoreCase))
    {
        relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
    }

    return relativePath;
}

32
После большого количества испытаний этот метод работал лучше всего для меня. Вы должны помнить, что Uri рассматривает папку, которая не заканчивается разделителем пути, как файл (используйте c: \ foo \ bar \ вместо c: \ foo \ bar, если bar - это папка).
— ВВС

6
Общее решение проблемы косых return relativeUri.ToString().Replace('/',Path.DirectorySeparatorChar);
— черт

4
Вы должны удалить относительный URI, созданный таким образом, чтобы получить правильный путь; представление .ToString () будет включать escape-последовательности, которые недопустимы и не нужны в пути.
— Имон Нербонн

3
добавил ниже после проверки arg if (fromPath.Last ()! = Path.DirectorySeparatorChar) {fromPath + = Path.DirectorySeparatorChar; } if (toPath.Last ()! = Path.DirectorySeparatorChar) {toPath + = Path.DirectorySeparatorChar; }
— Симон

8
Для меня это не возвращение относительного пути. Ибо c:\testи c:\test\abc.txtэто возвращает, test\abc.txtкоторый не является относительным по моему мнению. Я ожидал бы простоabc.txt
— Юрген д

51

Немного опоздал на вопрос, но мне просто нужна была и эта функция. Я согласен с DavidK, что, поскольку есть встроенная функция API, которая обеспечивает это, вы должны использовать ее. Вот управляемая оболочка для этого:

public static string GetRelativePath(string fromPath, string toPath)
{
    int fromAttr = GetPathAttribute(fromPath);
    int toAttr = GetPathAttribute(toPath);

    StringBuilder path = new StringBuilder(260); // MAX_PATH
    if(PathRelativePathTo(
        path,
        fromPath,
        fromAttr,
        toPath,
        toAttr) == 0)
    {
        throw new ArgumentException("Paths must have a common prefix");
    }
    return path.ToString();
}

private static int GetPathAttribute(string path)
{
    DirectoryInfo di = new DirectoryInfo(path);
    if (di.Exists)
    {
        return FILE_ATTRIBUTE_DIRECTORY;
    }

    FileInfo fi = new FileInfo(path);
    if(fi.Exists)
    {
        return FILE_ATTRIBUTE_NORMAL;
    }

    throw new FileNotFoundException();
}

private const int FILE_ATTRIBUTE_DIRECTORY = 0x10;
private const int FILE_ATTRIBUTE_NORMAL = 0x80;

[DllImport("shlwapi.dll", SetLastError = true)]
private static extern int PathRelativePathTo(StringBuilder pszPath, 
    string pszFrom, int dwAttrFrom, string pszTo, int dwAttrTo);

4
Я бы не бросил исключение, если файл или путь не существует, так как это может быть абсолютно законным случаем.
— ВВС

2
Так что же тогда вернет GetPathAttributes? Нет флага для «файл не существует», поэтому я не вижу никакой другой жизнеспособной опции, кроме как бросить, в противном случае вызывающая сторона получает ошибочную информацию.
— ctacke

2
Обратите внимание, что PathRelativePathTo возвращает FALSE, если не может быть создан относительный путь. В этом случае вы должны либо вернуть String.Empty, либо выдать исключение.
— Даниэль Роуз

4
Я нахожу это более понятным: он допускает код, такой как bool success = PathRelativePathTo (...), который, как мне кажется, легче понять, чем int, где вам нужно прочитать документацию о том, что означает int.
— Даниэль Роуз

4
Люди ... вы можете просто удалить весь GetPathAttribute, вы знаете. Пока вы абсолютно уверены, что аргументы, которые вы приводите, являются каталогами, вам просто нужно дать ему 0x10, и он будет работать с совершенно несуществующими путями. И в моем случае предпочтительным решением будет просто вернуть полный абсолютный целевой путь, а не выбрасывать это исключение.
— Nyerguds

31

Ответ .NET Core 2.0

.NET Core 2.0 имеет Path.GetRelativePath, который можно использовать так:

var relativePath = Path.GetRelativePath(
    @"C:\Program Files\Dummy Folder\MyProgram",
    @"C:\Program Files\Dummy Folder\MyProgram\Data\datafile1.dat");

В приведенном выше примере relativePathпеременная равна Data\datafile1.dat.

Альтернативный ответ .NET

Решение @ Dave не работает, когда пути к файлам не заканчиваются символом косой черты ( /), что может произойти, если путь является путем к каталогу. Мое решение решает эту проблему, а также использует Uri.UriSchemeFileконстанту вместо жесткого кодирования "FILE".

/// <summary>
/// Creates a relative path from one file or folder to another.
/// </summary>
/// <param name="fromPath">Contains the directory that defines the start of the relative path.</param>
/// <param name="toPath">Contains the path that defines the endpoint of the relative path.</param>
/// <returns>The relative path from the start directory to the end path.</returns>
/// <exception cref="ArgumentNullException"><paramref name="fromPath"/> or <paramref name="toPath"/> is <c>null</c>.</exception>
/// <exception cref="UriFormatException"></exception>
/// <exception cref="InvalidOperationException"></exception>
public static string GetRelativePath(string fromPath, string toPath)
{
    if (string.IsNullOrEmpty(fromPath))
    {
        throw new ArgumentNullException("fromPath");
    }

    if (string.IsNullOrEmpty(toPath))
    {
        throw new ArgumentNullException("toPath");
    }

    Uri fromUri = new Uri(AppendDirectorySeparatorChar(fromPath));
    Uri toUri = new Uri(AppendDirectorySeparatorChar(toPath));

    if (fromUri.Scheme != toUri.Scheme)
    {
        return toPath;
    }

    Uri relativeUri = fromUri.MakeRelativeUri(toUri);
    string relativePath = Uri.UnescapeDataString(relativeUri.ToString());

    if (string.Equals(toUri.Scheme, Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase))
    {
        relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
    }

    return relativePath;
}

private static string AppendDirectorySeparatorChar(string path)
{
    // Append a slash only if the path is a directory and does not have a slash.
    if (!Path.HasExtension(path) &&
        !path.EndsWith(Path.DirectorySeparatorChar.ToString()))
    {
        return path + Path.DirectorySeparatorChar;
    }

    return path;
}

Ответ Windows Interop

Существует Windows API под названием PathRelativePathToA, который можно использовать для поиска относительного пути. Обратите внимание, что пути к файлам или каталогам, которые вы передаете в функцию, должны существовать для ее работы.

var relativePath = PathExtended.GetRelativePath(
    @"C:\Program Files\Dummy Folder\MyProgram",
    @"C:\Program Files\Dummy Folder\MyProgram\Data\datafile1.dat");

public static class PathExtended
{
    private const int FILE_ATTRIBUTE_DIRECTORY = 0x10;
    private const int FILE_ATTRIBUTE_NORMAL = 0x80;
    private const int MaximumPath = 260;

    public static string GetRelativePath(string fromPath, string toPath)
    {
        var fromAttribute = GetPathAttribute(fromPath);
        var toAttribute = GetPathAttribute(toPath);

        var stringBuilder = new StringBuilder(MaximumPath);
        if (PathRelativePathTo(
            stringBuilder,
            fromPath,
            fromAttribute,
            toPath,
            toAttribute) == 0)
        {
            throw new ArgumentException("Paths must have a common prefix.");
        }

        return stringBuilder.ToString();
    }

    private static int GetPathAttribute(string path)
    {
        var directory = new DirectoryInfo(path);
        if (directory.Exists)
        {
            return FILE_ATTRIBUTE_DIRECTORY;
        }

        var file = new FileInfo(path);
        if (file.Exists)
        {
            return FILE_ATTRIBUTE_NORMAL;
        }

        throw new FileNotFoundException(
            "A file or directory with the specified path was not found.",
            path);
    }

    [DllImport("shlwapi.dll", SetLastError = true)]
    private static extern int PathRelativePathTo(
        StringBuilder pszPath,
        string pszFrom,
        int dwAttrFrom,
        string pszTo,
        int dwAttrTo);
}

1
Работает гораздо больше, чем можно было бы ожидать - я рекомендую это вместо ответа с наибольшим количеством голосов.
— The Mayer

Видя, как вы предполагаете, AltDirectorySeparatorCharесть возможность, не стоит ли AppendDirectorySeparatorCharэто тоже проверять?
— Охад Шнайдер

Кроме того, файл может быть без расширения, поэтому, хотя это может быть более удобным в большинстве случаев, он не позволяет указать этот случай. Возможно добавьте проверку, если запись файловой системы существует, и если так, проверьте, является ли это файлом или папкой. Если этого не существует, оставайтесь с этой логикой. Или, возможно, даже добавить какой-либо способ в подпись, чтобы указать, был ли предоставлен файл или каталог (например, 2 логических значения).
— Охад Шнайдер

Наконец, я бы бросил исключение, если схемы разные.
— Охад Шнайдер

25

В shlwapi.dll есть функция Win32 (C ++), которая делает именно то, что вы хотите: PathRelativePathTo()

Я не знаю ни одного способа получить доступ к этому из .NET кроме P / Invoke, хотя.


3
С какой частью это не поможет? Читая оригинальный пост, мне кажется, что PathRelativePathTo () делает то, что вы хотели, но это, вероятно, потому, что я что-то неправильно истолковал ...
— DavidK

3
Работает без нареканий. См. Pinvoke.net/default.aspx/shlwapi.PathRelativePathTo о том, как настроить P / Invoke.
— Joce

2
Спасибо! Я на самом деле искал решение C ++!
— NTDLS

2
Стоит отметить , что функции в shlwapi.dllнастоящее время не рекомендуется msdn.microsoft.com/en-us/library/windows/desktop/... "These functions are available through Windows XP Service Pack 2 (SP2) and Windows Server 2003. They might be altered or unavailable in subsequent versions of Windows."
— Basic

4
Я не читал эту страницу как заявление о том, что сам shlwapi.dll устарел: все, что он говорит, - то, что функции-оболочки из shlwapi.dll, перечисленные на странице, устарели. Сам по себе PathRelativePathTo () не упоминается на этой странице, и основная документация для PathRelativePathTo () не упоминает об устаревании, поэтому, насколько я вижу, она по-прежнему является допустимой функцией для вызова.
— DavidK

14

Если вы используете .NET Core 2.0,Path.GetRelativePath() доступно предоставление этой специфической функциональности:

        var relativeTo = @"C:\Program Files\Dummy Folder\MyProgram";
        var path = @"C:\Program Files\Dummy Folder\MyProgram\Data\datafile1.dat";

        string relativePath = System.IO.Path.GetRelativePath(relativeTo, path);

        System.Console.WriteLine(relativePath);
        // output --> Data\datafile1.dat 

В противном случае для полной платформы .NET (начиная с версии 4.7) рекомендуется использовать один из предложенных ответов.


9

Я использовал это в прошлом.

/// <summary>
/// Creates a relative path from one file
/// or folder to another.
/// </summary>
/// <param name="fromDirectory">
/// Contains the directory that defines the
/// start of the relative path.
/// </param>
/// <param name="toPath">
/// Contains the path that defines the
/// endpoint of the relative path.
/// </param>
/// <returns>
/// The relative path from the start
/// directory to the end path.
/// </returns>
/// <exception cref="ArgumentNullException"></exception>
public static string MakeRelative(string fromDirectory, string toPath)
{
  if (fromDirectory == null)
    throw new ArgumentNullException("fromDirectory");

  if (toPath == null)
    throw new ArgumentNullException("toPath");

  bool isRooted = (Path.IsPathRooted(fromDirectory) && Path.IsPathRooted(toPath));

  if (isRooted)
  {
    bool isDifferentRoot = (string.Compare(Path.GetPathRoot(fromDirectory), Path.GetPathRoot(toPath), true) != 0);

    if (isDifferentRoot)
      return toPath;
  }

  List<string> relativePath = new List<string>();
  string[] fromDirectories = fromDirectory.Split(Path.DirectorySeparatorChar);

  string[] toDirectories = toPath.Split(Path.DirectorySeparatorChar);

  int length = Math.Min(fromDirectories.Length, toDirectories.Length);

  int lastCommonRoot = -1;

  // find common root
  for (int x = 0; x < length; x++)
  {
    if (string.Compare(fromDirectories[x], toDirectories[x], true) != 0)
      break;

    lastCommonRoot = x;
  }

  if (lastCommonRoot == -1)
    return toPath;

  // add relative folders in from path
  for (int x = lastCommonRoot + 1; x < fromDirectories.Length; x++)
  {
    if (fromDirectories[x].Length > 0)
      relativePath.Add("..");
  }

  // add to folders to path
  for (int x = lastCommonRoot + 1; x < toDirectories.Length; x++)
  {
    relativePath.Add(toDirectories[x]);
  }

  // create relative path
  string[] relativeParts = new string[relativePath.Count];
  relativePath.CopyTo(relativeParts, 0);

  string newPath = string.Join(Path.DirectorySeparatorChar.ToString(), relativeParts);

  return newPath;
}

1
Я посмотрю на это, мне нужно время, чтобы проверить это. Спасибо
— неисправен

1
Я бы предложил использовать Path.GetFullPath () для успешного сравнения двух путей с относительными битами. Пример: c: \ a \ .. \ b против c: \ b против c: \ b \. \
— ВВС

5

Как указывает Алекс Браулт, особенно в Windows, абсолютный путь (с буквой диска и всем) однозначен и часто лучше.

Разве ваш OpenFileDialog не должен использовать обычную древовидную структуру браузера?

Чтобы получить определенную номенклатуру, RefDir - это каталог, относительно которого вы хотите указать путь; AbsName абсолютное имя пути , который вы хотите отобразить; и RelPath является результирующим относительным путем.

Возьмите первый из этих вариантов, который соответствует:

  • Если у вас разные буквы диска, нет относительного пути от RefDir до AbsName; Вы должны использовать AbsName.
  • Если AbsName находится в подкаталоге RefDir или является файлом в RefDir, то просто удалите RefDir из начала AbsName, чтобы создать RelPath; при желании добавьте «./» (или «. \», поскольку вы находитесь в Windows).
  • Найдите самый длинный общий префикс RefDir и AbsName (где D: \ Abc \ Def и D: \ Abc \ Default share D: \ Abc как самый длинный общий префикс; это должно быть сопоставление компонентов имени, а не простой самый длинный общий подстроки); назовите это LCP. Удалить LCP из AbsName и RefDir. Для каждого компонента пути, оставленного в (RefDir - LCP), добавьте ".. \" к (AbsName - LCP), чтобы получить RelPath.

Чтобы проиллюстрировать последнее правило (которое, безусловно, является самым сложным), начнем с:

RefDir = D:\Abc\Def\Ghi
AbsName = D:\Abc\Default\Karma\Crucible

затем

LCP = D:\Abc
(RefDir - LCP) = Def\Ghi
(Absname - LCP) = Default\Karma\Crucible
RelPath = ..\..\Default\Karma\Crucible

Пока я печатал, DavidK дал ответ, который предполагает, что вы не первый, кто нуждается в этой функции, и что для этой работы есть стандартная функция. Используй это. Но нет ничего плохого в том, чтобы быть способным продумывать свой путь из первых принципов.

За исключением того, что системы Unix не поддерживают буквы дисков (поэтому все всегда находится в одном и том же корневом каталоге, и поэтому первый маркер не имеет значения), такая же техника может быть использована в Unix.


4

Это долгий путь, но в классе System.Uri есть метод с именем MakeRelativeUri. Может быть, вы могли бы использовать это. Обидно, что System.IO.Path этого не имеет.


4

Как указано выше, .NET Core 2.x имеет реализацию Path.GetRelativePath.

Код ниже адаптирован из исходных кодов и отлично работает с .NET 4.7.1 Framework.

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

//Adapted from https://github.com/dotnet/corefx/blob/master/src/Common/src/CoreLib/System/IO/Path.cs#L697
// by Anton Krouglov

using System.Runtime.CompilerServices;
using System.Diagnostics;
using System.Text;
using Xunit;

namespace System.IO {
    // Provides methods for processing file system strings in a cross-platform manner.
    // Most of the methods don't do a complete parsing (such as examining a UNC hostname), 
    // but they will handle most string operations.
    public static class PathNetCore {

        /// <summary>
        /// Create a relative path from one path to another. Paths will be resolved before calculating the difference.
        /// Default path comparison for the active platform will be used (OrdinalIgnoreCase for Windows or Mac, Ordinal for Unix).
        /// </summary>
        /// <param name="relativeTo">The source path the output should be relative to. This path is always considered to be a directory.</param>
        /// <param name="path">The destination path.</param>
        /// <returns>The relative path or <paramref name="path"/> if the paths don't share the same root.</returns>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="relativeTo"/> or <paramref name="path"/> is <c>null</c> or an empty string.</exception>
        public static string GetRelativePath(string relativeTo, string path) {
            return GetRelativePath(relativeTo, path, StringComparison);
        }

        private static string GetRelativePath(string relativeTo, string path, StringComparison comparisonType) {
            if (string.IsNullOrEmpty(relativeTo)) throw new ArgumentNullException(nameof(relativeTo));
            if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path));
            Debug.Assert(comparisonType == StringComparison.Ordinal ||
                         comparisonType == StringComparison.OrdinalIgnoreCase);

            relativeTo = Path.GetFullPath(relativeTo);
            path = Path.GetFullPath(path);

            // Need to check if the roots are different- if they are we need to return the "to" path.
            if (!PathInternalNetCore.AreRootsEqual(relativeTo, path, comparisonType))
                return path;

            int commonLength = PathInternalNetCore.GetCommonPathLength(relativeTo, path,
                ignoreCase: comparisonType == StringComparison.OrdinalIgnoreCase);

            // If there is nothing in common they can't share the same root, return the "to" path as is.
            if (commonLength == 0)
                return path;

            // Trailing separators aren't significant for comparison
            int relativeToLength = relativeTo.Length;
            if (PathInternalNetCore.EndsInDirectorySeparator(relativeTo))
                relativeToLength--;

            bool pathEndsInSeparator = PathInternalNetCore.EndsInDirectorySeparator(path);
            int pathLength = path.Length;
            if (pathEndsInSeparator)
                pathLength--;

            // If we have effectively the same path, return "."
            if (relativeToLength == pathLength && commonLength >= relativeToLength) return ".";

            // We have the same root, we need to calculate the difference now using the
            // common Length and Segment count past the length.
            //
            // Some examples:
            //
            //  C:\Foo C:\Bar L3, S1 -> ..\Bar
            //  C:\Foo C:\Foo\Bar L6, S0 -> Bar
            //  C:\Foo\Bar C:\Bar\Bar L3, S2 -> ..\..\Bar\Bar
            //  C:\Foo\Foo C:\Foo\Bar L7, S1 -> ..\Bar

            StringBuilder
                sb = new StringBuilder(); //StringBuilderCache.Acquire(Math.Max(relativeTo.Length, path.Length));

            // Add parent segments for segments past the common on the "from" path
            if (commonLength < relativeToLength) {
                sb.Append("..");

                for (int i = commonLength + 1; i < relativeToLength; i++) {
                    if (PathInternalNetCore.IsDirectorySeparator(relativeTo[i])) {
                        sb.Append(DirectorySeparatorChar);
                        sb.Append("..");
                    }
                }
            }
            else if (PathInternalNetCore.IsDirectorySeparator(path[commonLength])) {
                // No parent segments and we need to eat the initial separator
                //  (C:\Foo C:\Foo\Bar case)
                commonLength++;
            }

            // Now add the rest of the "to" path, adding back the trailing separator
            int differenceLength = pathLength - commonLength;
            if (pathEndsInSeparator)
                differenceLength++;

            if (differenceLength > 0) {
                if (sb.Length > 0) {
                    sb.Append(DirectorySeparatorChar);
                }

                sb.Append(path, commonLength, differenceLength);
            }

            return sb.ToString(); //StringBuilderCache.GetStringAndRelease(sb);
        }

        // Public static readonly variant of the separators. The Path implementation itself is using
        // internal const variant of the separators for better performance.
        public static readonly char DirectorySeparatorChar = PathInternalNetCore.DirectorySeparatorChar;
        public static readonly char AltDirectorySeparatorChar = PathInternalNetCore.AltDirectorySeparatorChar;
        public static readonly char VolumeSeparatorChar = PathInternalNetCore.VolumeSeparatorChar;
        public static readonly char PathSeparator = PathInternalNetCore.PathSeparator;

        /// <summary>Returns a comparison that can be used to compare file and directory names for equality.</summary>
        internal static StringComparison StringComparison => StringComparison.OrdinalIgnoreCase;
    }

    /// <summary>Contains internal path helpers that are shared between many projects.</summary>
    internal static class PathInternalNetCore {
        internal const char DirectorySeparatorChar = '\\';
        internal const char AltDirectorySeparatorChar = '/';
        internal const char VolumeSeparatorChar = ':';
        internal const char PathSeparator = ';';

        internal const string ExtendedDevicePathPrefix = @"\\?\";
        internal const string UncPathPrefix = @"\\";
        internal const string UncDevicePrefixToInsert = @"?\UNC\";
        internal const string UncExtendedPathPrefix = @"\\?\UNC\";
        internal const string DevicePathPrefix = @"\\.\";

        //internal const int MaxShortPath = 260;

        // \\?\, \\.\, \??\
        internal const int DevicePrefixLength = 4;

        /// <summary>
        /// Returns true if the two paths have the same root
        /// </summary>
        internal static bool AreRootsEqual(string first, string second, StringComparison comparisonType) {
            int firstRootLength = GetRootLength(first);
            int secondRootLength = GetRootLength(second);

            return firstRootLength == secondRootLength
                   && string.Compare(
                       strA: first,
                       indexA: 0,
                       strB: second,
                       indexB: 0,
                       length: firstRootLength,
                       comparisonType: comparisonType) == 0;
        }

        /// <summary>
        /// Gets the length of the root of the path (drive, share, etc.).
        /// </summary>
        internal static int GetRootLength(string path) {
            int i = 0;
            int volumeSeparatorLength = 2; // Length to the colon "C:"
            int uncRootLength = 2; // Length to the start of the server name "\\"

            bool extendedSyntax = path.StartsWith(ExtendedDevicePathPrefix);
            bool extendedUncSyntax = path.StartsWith(UncExtendedPathPrefix);
            if (extendedSyntax) {
                // Shift the position we look for the root from to account for the extended prefix
                if (extendedUncSyntax) {
                    // "\\" -> "\\?\UNC\"
                    uncRootLength = UncExtendedPathPrefix.Length;
                }
                else {
                    // "C:" -> "\\?\C:"
                    volumeSeparatorLength += ExtendedDevicePathPrefix.Length;
                }
            }

            if ((!extendedSyntax || extendedUncSyntax) && path.Length > 0 && IsDirectorySeparator(path[0])) {
                // UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo")

                i = 1; //  Drive rooted (\foo) is one character
                if (extendedUncSyntax || (path.Length > 1 && IsDirectorySeparator(path[1]))) {
                    // UNC (\\?\UNC\ or \\), scan past the next two directory separators at most
                    // (e.g. to \\?\UNC\Server\Share or \\Server\Share\)
                    i = uncRootLength;
                    int n = 2; // Maximum separators to skip
                    while (i < path.Length && (!IsDirectorySeparator(path[i]) || --n > 0)) i++;
                }
            }
            else if (path.Length >= volumeSeparatorLength &&
                     path[volumeSeparatorLength - 1] == PathNetCore.VolumeSeparatorChar) {
                // Path is at least longer than where we expect a colon, and has a colon (\\?\A:, A:)
                // If the colon is followed by a directory separator, move past it
                i = volumeSeparatorLength;
                if (path.Length >= volumeSeparatorLength + 1 && IsDirectorySeparator(path[volumeSeparatorLength])) i++;
            }

            return i;
        }

        /// <summary>
        /// True if the given character is a directory separator.
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal static bool IsDirectorySeparator(char c) {
            return c == PathNetCore.DirectorySeparatorChar || c == PathNetCore.AltDirectorySeparatorChar;
        }

        /// <summary>
        /// Get the common path length from the start of the string.
        /// </summary>
        internal static int GetCommonPathLength(string first, string second, bool ignoreCase) {
            int commonChars = EqualStartingCharacterCount(first, second, ignoreCase: ignoreCase);

            // If nothing matches
            if (commonChars == 0)
                return commonChars;

            // Or we're a full string and equal length or match to a separator
            if (commonChars == first.Length
                && (commonChars == second.Length || IsDirectorySeparator(second[commonChars])))
                return commonChars;

            if (commonChars == second.Length && IsDirectorySeparator(first[commonChars]))
                return commonChars;

            // It's possible we matched somewhere in the middle of a segment e.g. C:\Foodie and C:\Foobar.
            while (commonChars > 0 && !IsDirectorySeparator(first[commonChars - 1]))
                commonChars--;

            return commonChars;
        }

        /// <summary>
        /// Gets the count of common characters from the left optionally ignoring case
        /// </summary>
        internal static unsafe int EqualStartingCharacterCount(string first, string second, bool ignoreCase) {
            if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(second)) return 0;

            int commonChars = 0;

            fixed (char* f = first)
            fixed (char* s = second) {
                char* l = f;
                char* r = s;
                char* leftEnd = l + first.Length;
                char* rightEnd = r + second.Length;

                while (l != leftEnd && r != rightEnd
                                    && (*l == *r || (ignoreCase &&
                                                     char.ToUpperInvariant((*l)) == char.ToUpperInvariant((*r))))) {
                    commonChars++;
                    l++;
                    r++;
                }
            }

            return commonChars;
        }

        /// <summary>
        /// Returns true if the path ends in a directory separator.
        /// </summary>
        internal static bool EndsInDirectorySeparator(string path)
            => path.Length > 0 && IsDirectorySeparator(path[path.Length - 1]);
    }

    /// <summary> Tests for PathNetCore.GetRelativePath </summary>
    public static class GetRelativePathTests {
        [Theory]
        [InlineData(@"C:\", @"C:\", @".")]
        [InlineData(@"C:\a", @"C:\a\", @".")]
        [InlineData(@"C:\A", @"C:\a\", @".")]
        [InlineData(@"C:\a\", @"C:\a", @".")]
        [InlineData(@"C:\", @"C:\b", @"b")]
        [InlineData(@"C:\a", @"C:\b", @"..\b")]
        [InlineData(@"C:\a", @"C:\b\", @"..\b\")]
        [InlineData(@"C:\a\b", @"C:\a", @"..")]
        [InlineData(@"C:\a\b", @"C:\a\", @"..")]
        [InlineData(@"C:\a\b\", @"C:\a", @"..")]
        [InlineData(@"C:\a\b\", @"C:\a\", @"..")]
        [InlineData(@"C:\a\b\c", @"C:\a\b", @"..")]
        [InlineData(@"C:\a\b\c", @"C:\a\b\", @"..")]
        [InlineData(@"C:\a\b\c", @"C:\a", @"..\..")]
        [InlineData(@"C:\a\b\c", @"C:\a\", @"..\..")]
        [InlineData(@"C:\a\b\c\", @"C:\a\b", @"..")]
        [InlineData(@"C:\a\b\c\", @"C:\a\b\", @"..")]
        [InlineData(@"C:\a\b\c\", @"C:\a", @"..\..")]
        [InlineData(@"C:\a\b\c\", @"C:\a\", @"..\..")]
        [InlineData(@"C:\a\", @"C:\b", @"..\b")]
        [InlineData(@"C:\a", @"C:\a\b", @"b")]
        [InlineData(@"C:\a", @"C:\A\b", @"b")]
        [InlineData(@"C:\a", @"C:\b\c", @"..\b\c")]
        [InlineData(@"C:\a\", @"C:\a\b", @"b")]
        [InlineData(@"C:\", @"D:\", @"D:\")]
        [InlineData(@"C:\", @"D:\b", @"D:\b")]
        [InlineData(@"C:\", @"D:\b\", @"D:\b\")]
        [InlineData(@"C:\a", @"D:\b", @"D:\b")]
        [InlineData(@"C:\a\", @"D:\b", @"D:\b")]
        [InlineData(@"C:\ab", @"C:\a", @"..\a")]
        [InlineData(@"C:\a", @"C:\ab", @"..\ab")]
        [InlineData(@"C:\", @"\\LOCALHOST\Share\b", @"\\LOCALHOST\Share\b")]
        [InlineData(@"\\LOCALHOST\Share\a", @"\\LOCALHOST\Share\b", @"..\b")]
        //[PlatformSpecific(TestPlatforms.Windows)]  // Tests Windows-specific paths
        public static void GetRelativePath_Windows(string relativeTo, string path, string expected) {
            string result = PathNetCore.GetRelativePath(relativeTo, path);
            Assert.Equal(expected, result);

            // Check that we get the equivalent path when the result is combined with the sources
            Assert.Equal(
                Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar),
                Path.GetFullPath(Path.Combine(Path.GetFullPath(relativeTo), result))
                    .TrimEnd(Path.DirectorySeparatorChar),
                ignoreCase: true,
                ignoreLineEndingDifferences: false,
                ignoreWhiteSpaceDifferences: false);
        }
    }
}

Вау, спасибо большое! Это прекрасно работает с .NET Framework 4.6.1. Этот ответ должен быть одобрен или даже принят как решение.
— j00hi

Спасибо! С незначительными изменениями он также работает в .NET Framework 4.5.2
— Bja

3

Я использую это:

public static class StringExtensions
{
  /// <summary>
  /// Creates a relative path from one file or folder to another.
  /// </summary>
  /// <param name="absPath">Absolute path.</param>
  /// <param name="relTo">Directory that defines the start of the relative path.</param> 
  /// <returns>The relative path from the start directory to the end path.</returns>
  public static string MakeRelativePath(this string absPath, string relTo)
  {
      string[] absParts = absPath.Split(Path.DirectorySeparatorChar);
      string[] relParts = relTo.Split(Path.DirectorySeparatorChar);

      // Get the shortest of the two paths
      int len = absParts.Length < relParts.Length
          ? absParts.Length : relParts.Length;

      // Use to determine where in the loop we exited
      int lastCommonRoot = -1;
      int index;

      // Find common root
      for (index = 0; index < len; index++)
      {
          if (absParts[index].Equals(relParts[index], StringComparison.OrdinalIgnoreCase))
              lastCommonRoot = index;
          else 
            break;
      }

      // If we didn't find a common prefix then throw
      if (lastCommonRoot == -1)
          throw new ArgumentException("The path of the two files doesn't have any common base.");

      // Build up the relative path
      var relativePath = new StringBuilder();

      // Add on the ..
      for (index = lastCommonRoot + 1; index < relParts.Length; index++)
      {
        relativePath.Append("..");
        relativePath.Append(Path.DirectorySeparatorChar);
      }

      // Add on the folders
      for (index = lastCommonRoot + 1; index < absParts.Length - 1; index++)
      {
        relativePath.Append(absParts[index]);
        relativePath.Append(Path.DirectorySeparatorChar);
      }
      relativePath.Append(absParts[absParts.Length - 1]);

      return relativePath.ToString();
  }
}

3

Использование:

RelPath = AbsPath.Replace(ApplicationPath, ".")

для узкого круга случаев это будет работать блестяще! Я собираюсь использовать это! Я думаю, что эти другие парни хотят универсального решения для проверки ошибок, обработки краев. но если это все, что вам нужно, это просто!
— ДанО

Я закончил тем, что использовал path.Replace(rootPath.TrimEnd('\\') + "\\", "").
— Конард

2

Если вы уверены, что ваш абсолютный путь 2 всегда относительно абсолютного пути, просто удалите первые N символов из path2, где N - длина пути1.


2

Вы хотите использовать CommonPathметод этого RelativePathкласса. Если у вас есть общий путь, просто удалите его с пути, который вы хотите отобразить.

Namespace IO.Path

    Public NotInheritable Class RelativePath

        Private Declare Function PathRelativePathTo Lib "shlwapi" Alias "PathRelativePathToA" ( _
            ByVal pszPath As String, _
            ByVal pszFrom As String, _
            ByVal dwAttrFrom As Integer, _
            ByVal pszTo As String, _
            ByVal dwAttrTo As Integer) As Integer

        Private Declare Function PathCanonicalize Lib "shlwapi" Alias "PathCanonicalizeA" ( _
            ByVal pszBuf As String, _
            ByVal pszPath As String) As Integer

        Private Const FILE_ATTRIBUTE_DIRECTORY As Short = &H10S

        Private Const MAX_PATH As Short = 260

        Private _path As String
        Private _isDirectory As Boolean

#Region " Constructors "

        Public Sub New()

        End Sub

        Public Sub New(ByVal path As String)
            _path = path
        End Sub

        Public Sub New(ByVal path As String, ByVal isDirectory As Boolean)
            _path = path
            _isDirectory = isDirectory
        End Sub

#End Region

        Private Shared Function StripNulls(ByVal value As String) As String
            StripNulls = value
            If (InStr(value, vbNullChar) > 0) Then
                StripNulls = Left(value, InStr(value, vbNullChar) - 1)
            End If
        End Function

        Private Shared Function TrimCurrentDirectory(ByVal path As String) As String
            TrimCurrentDirectory = path
            If Len(path) >= 2 And Left(path, 2) = ".\" Then
                TrimCurrentDirectory = Mid(path, 3)
            End If
        End Function

        ''' <summary>
        ''' 3. conforming to general principles: conforming to accepted principles or standard practice
        ''' </summary>
        Public Shared Function Canonicalize(ByVal path As String) As String
            Dim sPath As String

            sPath = New String(Chr(0), MAX_PATH)

            If PathCanonicalize(sPath, path) = 0 Then
                Canonicalize = vbNullString
            Else
                Canonicalize = StripNulls(sPath)
            End If

        End Function

        ''' <summary>
        ''' Returns the most common path between two paths.
        ''' </summary>
        ''' <remarks>
        ''' <para>returns the path that is common between two paths</para>
        ''' <para>c:\FolderA\FolderB\FolderC</para>
        '''   c:\FolderA\FolderD\FolderE\File.Ext
        ''' 
        '''   results in:
        '''       c:\FolderA\
        ''' </remarks>
        Public Shared Function CommonPath(ByVal path1 As String, ByVal path2 As String) As String
            'returns the path that is common between two paths
            '
            '   c:\FolderA\FolderB\FolderC
            '   c:\FolderA\FolderD\FolderE\File.Ext
            '
            '   results in:
            '       c:\FolderA\

            Dim sResult As String = String.Empty
            Dim iPos1, iPos2 As Integer
            path1 = Canonicalize(path1)
            path2 = Canonicalize(path2)
            Do
                If Left(path1, iPos1) = Left(path2, iPos2) Then
                    sResult = Left(path1, iPos1)
                End If
                iPos1 = InStr(iPos1 + 1, path1, "\")
                iPos2 = InStr(iPos2 + 1, path1, "\")
            Loop While Left(path1, iPos1) = Left(path2, iPos2)

            Return sResult

        End Function

        Public Function CommonPath(ByVal path As String) As String
            Return CommonPath(_path, path)
        End Function

        Public Shared Function RelativePathTo(ByVal source As String, ByVal isSourceDirectory As Boolean, ByVal target As String, ByVal isTargetDirectory As Boolean) As String
            'DEVLIB
            '   05/23/05  1:47PM - Fixed call to PathRelativePathTo, iTargetAttribute is now passed to dwAttrTo instead of IsTargetDirectory.
            '       For Visual Basic 6.0, the fix does not change testing results,
            '           because when the Boolean IsTargetDirectory is converted to the Long dwAttrTo it happens to contain FILE_ATTRIBUTE_DIRECTORY,
            '
            Dim sRelativePath As String
            Dim iSourceAttribute, iTargetAttribute As Integer

            sRelativePath = New String(Chr(0), MAX_PATH)
            source = Canonicalize(source)
            target = Canonicalize(target)

            If isSourceDirectory Then
                iSourceAttribute = FILE_ATTRIBUTE_DIRECTORY
            End If

            If isTargetDirectory Then
                iTargetAttribute = FILE_ATTRIBUTE_DIRECTORY
            End If

            If PathRelativePathTo(sRelativePath, source, iSourceAttribute, target, iTargetAttribute) = 0 Then
                RelativePathTo = vbNullString
            Else
                RelativePathTo = TrimCurrentDirectory(StripNulls(sRelativePath))
            End If

        End Function

        Public Function RelativePath(ByVal target As String) As String
            Return RelativePathTo(_path, _isDirectory, target, False)
        End Function

    End Class

End Namespace

1

Я бы разделил оба ваших пути на уровне каталогов. Оттуда найдите точку расхождения и вернитесь к папке сборки, добавляя «../» каждый раз, когда вы передаете каталог.

Имейте в виду, однако, что абсолютный путь работает везде и обычно легче читать, чем относительный. Я лично не показал бы пользователю относительный путь, если бы это не было абсолютно необходимо.


Полностью согласен - во многих случаях относительный путь может быть полным путем, например, вашим общим корнем был диск - c: \ - так что вам все равно придется обрабатывать этот случай.
— Stephbu

1

Если вы знаете, что toPath содержится в fromPath, вы можете упростить его. Я опущу утверждения для краткости.

public static string MakeRelativePath(string fromPath, string toPath)
{
    // use Path.GetFullPath to canonicalise the paths (deal with multiple directory seperators, etc)
    return Path.GetFullPath(toPath).Substring(Path.GetFullPath(fromPath).Length + 1);
}

2
Что делать, если они находятся в разных папках? Это не добавляет "..". Что, если один из путей уже содержит «..»? Это вернуло бы неверный уровень относительного пути. Что, если файл A находится в «MyFolder», а файл B - в «MyLunchbox» - этот метод не знает о символах разделителя каталогов, поэтому он просто подумает, что «Lunchbox \ File» был правильным путем. Это ужасно
— BrainSlugs83

1

Функция, которая использует URI, вернула «почти» относительный путь. Это включало каталог, который непосредственно содержит файл, относительный путь которого я хотел получить.

Некоторое время назад я написал простую функцию, которая возвращает относительный путь к папке или файлу, и даже если он находится на другом диске, он также включает в себя букву диска.

Взгляни, пожалуйста:

    public static string GetRelativePath(string BasePath, string AbsolutePath)
    {
        char Separator = Path.DirectorySeparatorChar;
        if (string.IsNullOrWhiteSpace(BasePath)) BasePath = Directory.GetCurrentDirectory();
        var ReturnPath = "";
        var CommonPart = "";
        var BasePathFolders = BasePath.Split(Separator);
        var AbsolutePathFolders = AbsolutePath.Split(Separator);
        var i = 0;
        while (i < BasePathFolders.Length & i < AbsolutePathFolders.Length)
        {
            if (BasePathFolders[i].ToLower() == AbsolutePathFolders[i].ToLower())
            {
                CommonPart += BasePathFolders[i] + Separator;
            }
            else
            {
                break;
            }
            i += 1;
        }
        if (CommonPart.Length > 0)
        {
            var parents = BasePath.Substring(CommonPart.Length - 1).Split(Separator);
            foreach (var ParentDir in parents)
            {
                if (!string.IsNullOrEmpty(ParentDir))
                    ReturnPath += ".." + Separator;
            }
        }
        ReturnPath += AbsolutePath.Substring(CommonPart.Length);
        return ReturnPath;
    }

1

Если у вас есть текстовое поле только для чтения, не могли бы вы не сделать его меткой и установить AutoEllipsis = true?

в качестве альтернативы есть посты с кодом для генерации автоэллипса самостоятельно: (это делает это для сетки, вместо этого вам нужно будет передать ширину для текстового поля. Это не совсем правильно, так как отрубает немного больше, чем необходимо , и я не удосужился найти неправильные вычисления. Было бы достаточно легко изменить, чтобы удалить первую часть каталога, а не последнюю, если вы хотите.

Private Function AddEllipsisPath(ByVal text As String, ByVal colIndex As Integer, ByVal grid As DataGridView) As String
    'Get the size with the column's width 
    Dim colWidth As Integer = grid.Columns(colIndex).Width

    'Calculate the dimensions of the text with the current font
    Dim textSize As SizeF = MeasureString(text, grid.Font)

    Dim rawText As String = text
    Dim FileNameLen As Integer = text.Length - text.LastIndexOf("\")
    Dim ReplaceWith As String = "\..."

    Do While textSize.Width > colWidth
        ' Trim to make room for the ellipsis
        Dim LastFolder As Integer = rawText.LastIndexOf("\", rawText.Length - FileNameLen - 1)

        If LastFolder < 0 Then
            Exit Do
        End If

        rawText = rawText.Substring(0, LastFolder) + ReplaceWith + rawText.Substring(rawText.Length - FileNameLen)

        If ReplaceWith.Length > 0 Then
            FileNameLen += 4
            ReplaceWith = ""
        End If
        textSize = MeasureString(rawText, grid.Font)
    Loop

    Return rawText
End Function

Private Function MeasureString(ByVal text As String, ByVal fontInfo As Font) As SizeF
    Dim size As SizeF
    Dim emSize As Single = fontInfo.Size
    If emSize = 0 Then emSize = 12

    Dim stringFont As New Font(fontInfo.Name, emSize)

    Dim bmp As New Bitmap(1000, 100)
    Dim g As Graphics = Graphics.FromImage(bmp)

    size = g.MeasureString(text, stringFont)
    g.Dispose()
    Return size
End Function

0
    public static string ToRelativePath(string filePath, string refPath)
    {
        var pathNormalized = Path.GetFullPath(filePath);

        var refNormalized = Path.GetFullPath(refPath);
        refNormalized = refNormalized.TrimEnd('\\', '/');

        if (!pathNormalized.StartsWith(refNormalized))
            throw new ArgumentException();
        var res = pathNormalized.Substring(refNormalized.Length + 1);
        return res;
    }

0

Это должно работать:

private string rel(string path) {
  string[] cwd  = new Regex(@"[\\]").Split(Directory.GetCurrentDirectory());
  string[] fp   = new Regex(@"[\\]").Split(path);

  int common = 0;

  for (int n = 0; n < fp.Length; n++) {
    if (n < cwd.Length && n < fp.Length && cwd[n] == fp[n]) {
      common++;
    }
  }

  if (common > 0) {
    List<string> rp = new List<string>();

    for (int n = 0; n < (cwd.Length - common); n++) {
      rp.Add("..");
    }

    for (int n = common; n < fp.Length; n++) {
      rp.Add(fp[n]);
    }

    return String.Join("/", rp.ToArray());
  } else {
    return String.Join("/", fp);
  }
}

0

Способ с Uri не работал на системах Linux / MacOS. Путь '/ var / www / root' не может быть преобразован в Uri. Более универсальный способ - все делать руками.

public static string MakeRelativePath(string fromPath, string toPath, string sep = "/")
{
    var fromParts = fromPath.Split(new[] { '/', '\\'},
        StringSplitOptions.RemoveEmptyEntries);
    var toParts = toPath.Split(new[] { '/', '\\'},
        StringSplitOptions.RemoveEmptyEntries);

    var matchedParts = fromParts
        .Zip(toParts, (x, y) => string.Compare(x, y, true) == 0)
        .TakeWhile(x => x).Count();

    return string.Join("", Enumerable.Range(0, fromParts.Length - matchedParts)
        .Select(x => ".." + sep)) +
            string.Join(sep, toParts.Skip(matchedParts));
}        

PS: я использую "/" в качестве значения по умолчанию разделителя вместо Path.DirectorySeparatorChar, потому что результат этого метода используется как URI в моем приложении.


0

вот мой

public static string RelativePathTo(this System.IO.DirectoryInfo @this, string to)
{
    var rgFrom = @this.FullName.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
    var rgTo = to.Split(new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
    var cSame = rgFrom.TakeWhile((p, i) => i < rgTo.Length && string.Equals(p, rgTo[i])).Count();

    return Path.Combine(
        Enumerable.Range(0, rgFrom.Length - cSame)
        .Select(_ => "..")
        .Concat(rgTo.Skip(cSame))
        .ToArray()
    );
}

0

Играть с чем-то вроде:

private String GetRelativePath(Int32 level, String directory, out String errorMessage) {
        if (level < 0 || level > 5) {
            errorMessage = "Find some more smart input data";
            return String.Empty;
        }
        // ==========================
        while (level != 0) {
            directory = Path.GetDirectoryName(directory);
            level -= 1;
        }
        // ==========================
        errorMessage = String.Empty;
        return directory;
    }

И проверить это

[Test]
    public void RelativeDirectoryPathTest() {
        var relativePath =
            GetRelativePath(3, AppDomain.CurrentDomain.BaseDirectory, out var errorMessage);
        Console.WriteLine(relativePath);
        if (String.IsNullOrEmpty(errorMessage) == false) {
            Console.WriteLine(errorMessage);
            Assert.Fail("Can not find relative path");
        }
    }

0

В ASP.NET Core 2, если вы хотите , относительный путь к bin\Debug\netcoreapp2.2вам можно использовать следующую комбинацию:

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
public class RenderingService : IRenderingService
{

    private readonly IHostingEnvironment _hostingEnvironment;
    public RenderingService(IHostingEnvironment hostingEnvironment)
    {
    _hostingEnvironment = hostingEnvironment;
    }

    public string RelativeAssemblyDirectory()
    {
        var contentRootPath = _hostingEnvironment.ContentRootPath;
        string executingAssemblyDirectoryAbsolutePath = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
        string executingAssemblyDirectoryRelativePath = System.IO.Path.GetRelativePath(contentRootPath, executingAssemblyDirectoryAbsolutePath);
        return executingAssemblyDirectoryRelativePath;
    }
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.