Path.Combine для URL-адресов?

Path.Combine удобен, но есть ли аналогичная функция в платформе .NET для URL-адрес?

Я ищу синтаксис вот так:

Url.Combine("http://MyUrl.com/", "/Images/Image.jpg")

который вернется:

"http://MyUrl.com/Images/Image.jpg"

+1168
16 дек. '08 в 21:42
источник поделиться
34 ответа
  • 1
  • 2

Выше есть комментарий Тодда Меньера о том, что Flurl включает в себя Url.Combine.

Больше деталей:

Url.Combine - это, по сути, Path.Combine для URL, обеспечивающий один и только один символ-разделитель между частями:

var url = Url.Combine(
    "http://foo.com/",
    "/too/", "/many/", "/slashes/",
    "too", "few?",
    "x=1", "y=2"
// result: "http://www.foo.com/too/many/slashes/too/few?x=1&y=2" 

Получить Flurl.Http на NuGet:

PM> Install-Package Flurl.Http

Или получите автономный построитель URL без функций HTTP:

PM> Install-Package Flurl

+35
13 апр. '18 в 6:00
источник

Uri есть конструктор, который должен сделать это за вас: new Uri(Uri baseUri, string relativeUri)

Вот пример:

Uri baseUri = new Uri("http://www.contoso.com");
Uri myUri = new Uri(baseUri, "catalog/shownew.htm");

Примечание редактора: будьте осторожны, этот метод не работает должным образом. В некоторых случаях он может вырезать часть baseUri. Смотрите комментарии и другие ответы.

+1109
06 окт. '09 в 19:37
источник

Вы используете Uri.TryCreate( ... ):

Uri result = null;

if (Uri.TryCreate(new Uri("http://msdn.microsoft.com/en-us/library/"), "/en-us/library/system.uri.trycreate.aspx", out result))
{
    Console.WriteLine(result);
}

Вернется:

http://msdn.microsoft.com/en-us/library/system.uri.trycreate.aspx

+141
16 дек. '08 в 21:49
источник

Это может быть достаточно простым решением:

public static string Combine(string uri1, string uri2)
{
    uri1 = uri1.TrimEnd('/');
    uri2 = uri2.TrimStart('/');
    return string.Format("{0}/{1}", uri1, uri2);
}
+141
25 сент. '09 в 10:29
источник

Здесь уже есть отличные ответы. На основе предложения mdsharpe, здесь используется метод расширения, который можно легко использовать, когда вы хотите иметь дело с экземплярами Uri:

using System;
using System.Linq;

public static class UriExtensions
{
    public static Uri Append(this Uri uri, params string[] paths)
    {
        return new Uri(paths.Aggregate(uri.AbsoluteUri, (current, path) => string.Format("{0}/{1}", current.TrimEnd('/'), path.TrimStart('/'))));
    }
}

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

var url = new Uri("http://example.com/subpath/").Append("/part1/", "part2").AbsoluteUri;

Это приведет к http://example.com/subpath/part1/part2

+122
03 нояб. '11 в 10:20
источник

Ответ Райана Кука близок к тому, что мне нужно, и может быть более подходящим для других разработчиков. Тем не менее, он добавляет http://в начало строки и в целом он делает немного больше форматирования, чем я после.

Кроме того, для моих случаев использования определение относительных путей не важно.

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

С#

public string UrlCombine(string url1, string url2)
{
    if (url1.Length == 0) {
        return url2;
    }

    if (url2.Length == 0) {
        return url1;
    }

    url1 = url1.TrimEnd('/', '\\');
    url2 = url2.TrimStart('/', '\\');

    return string.Format("{0}/{1}", url1, url2);
}

VB.NET

Public Function UrlCombine(ByVal url1 As String, ByVal url2 As String) As String
    If url1.Length = 0 Then
        Return url2
    End If

    If url2.Length = 0 Then
        Return url1
    End If

    url1 = url1.TrimEnd("/"c, "\"c)
    url2 = url2.TrimStart("/"c, "\"c)

    Return String.Format("{0}/{1}", url1, url2)
End Function

Этот код проходит следующий тест, который происходит в VB:

<TestMethod()> Public Sub UrlCombineTest()
    Dim target As StringHelpers = New StringHelpers()

    Assert.IsTrue(target.UrlCombine("test1", "test2") = "test1/test2")
    Assert.IsTrue(target.UrlCombine("test1/", "test2") = "test1/test2")
    Assert.IsTrue(target.UrlCombine("test1", "/test2") = "test1/test2")
    Assert.IsTrue(target.UrlCombine("test1/", "/test2") = "test1/test2")
    Assert.IsTrue(target.UrlCombine("/test1/", "/test2/") = "/test1/test2/")
    Assert.IsTrue(target.UrlCombine("", "/test2/") = "/test2/")
    Assert.IsTrue(target.UrlCombine("/test1/", "") = "/test1/")
End Sub
+84
10 мая '10 в 21:53
источник

Исходя из предоставленного вами примера URL, я предполагаю, что вы хотите объединить URL, относящиеся к вашему сайту.

Исходя из этого предположения, я предложу это решение как наиболее подходящий ответ на ваш вопрос: "Path.Combine удобен, есть ли подобная функция в структуре для URL-адресов?"

Поскольку в структуре для URL-адресов есть аналогичная функция, я предлагаю правильный метод: "VirtualPathUtility.Combine". Вот ссылка на MSDN: VirtualPathUtility.Combine Метод

Есть одно предостережение: я считаю, что это работает только для URL, относящихся к вашему сайту (то есть вы не можете использовать его для создания ссылок на другой веб-сайт. Например, var url = VirtualPathUtility.Combine("www.google.com", "accounts/widgets");).

+34
28 мар. '10 в 0:21
источник

Path.Combine не работает для меня, потому что могут быть такие символы, как "|" в аргументах QueryString и, следовательно, URL, что приведет к ArgumentException.

Сначала я попробовал новый подход Uri(Uri baseUri, string relativeUri), который мне не удался из-за таких URI, как http://www.mediawiki.org/wiki/Special:SpecialPages:

new Uri(new Uri("http://www.mediawiki.org/wiki/"), "Special:SpecialPages")

приведет к Special: SpecialPages из-за двоеточия после Special, обозначающего схему.

Поэтому мне, наконец, пришлось выбрать маршрут mdsharpe/Brian MacKays, и я разработал его немного дальше, чтобы работать с несколькими частями URI:

public static string CombineUri(params string[] uriParts)
{
    string uri = string.Empty;
    if (uriParts != null && uriParts.Length > 0)
    {
        char[] trims = new char[] { '\\', '/' };
        uri = (uriParts[0] ?? string.Empty).TrimEnd(trims);
        for (int i = 1; i < uriParts.Length; i++)
        {
            uri = string.Format("{0}/{1}", uri.TrimEnd(trims), (uriParts[i] ?? string.Empty).TrimStart(trims));
        }
    }
    return uri;
}

Использование: CombineUri("http://www.mediawiki.org/", "wiki", "Special:SpecialPages")

+30
15 июл. '11 в 8:17
источник
Path.Combine("Http://MyUrl.com/", "/Images/Image.jpg").Replace("\\", "/")
+22
11 янв. '11 в 21:41
источник

Я просто собрал небольшой метод расширения:

public static string UriCombine (this string val, string append)
        {
            if (String.IsNullOrEmpty(val)) return append;
            if (String.IsNullOrEmpty(append)) return val;
            return val.TrimEnd('/') + "/" + append.TrimStart('/');
        }

Это можно использовать так:

"www.example.com/".UriCombine("/images").UriCombine("first.jpeg");
+17
25 нояб. '10 в 8:43
источник

Остроумный пример, Райан, чтобы закончить со ссылкой на функцию. Молодцы.

Одна рекомендация Брайан: если вы закроете этот код в функции, вы можете использовать UriBuilder для обвязывания базового URL до вызова TryCreate.

В противном случае базовый URL ДОЛЖЕН включать схему (где UriBuilder будет считать http://). Просто мысль:

public string CombineUrl(string baseUrl, string relativeUrl) {
    UriBuilder baseUri = new UriBuilder(baseUrl);
    Uri newUri;

    if (Uri.TryCreate(baseUri.Uri, relativeUrl, out newUri))
        return newUri.ToString();
    else
        throw new ArgumentException("Unable to combine specified url values");
}
+12
30 июл. '09 в 15:08
источник

Объединение нескольких частей URL может быть немного сложнее. Вы можете использовать двухпараметрический конструктор Uri(baseUri, relativeUri) или служебную функцию Uri.TryCreate().

В любом случае вы можете в итоге вернуть неверный результат, потому что эти методы продолжают baseUri относительные части из первого параметра baseUri, то есть из чего-то вроде http://google.com/some/thing в http://google.com.

Чтобы иметь возможность объединить несколько частей в окончательный URL-адрес, вы можете скопировать две функции ниже:

    public static string Combine(params string[] parts)
    {
        if (parts == null || parts.Length == 0) return string.Empty;

        var urlBuilder = new StringBuilder();
        foreach (var part in parts)
        {
            var tempUrl = tryCreateRelativeOrAbsolute(part);
            urlBuilder.Append(tempUrl);
        }
        return VirtualPathUtility.RemoveTrailingSlash(urlBuilder.ToString());
    }

    private static string tryCreateRelativeOrAbsolute(string s)
    {
        System.Uri uri;
        System.Uri.TryCreate(s, UriKind.RelativeOrAbsolute, out uri);
        string tempUrl = VirtualPathUtility.AppendTrailingSlash(uri.ToString());
        return tempUrl;
    }

Полный код с модульными тестами для демонстрации использования можно найти по адресу https://uricombine.codeplex.com/SourceControl/latest#UriCombine/Uri.cs.

У меня есть модульные тесты, чтобы охватить три наиболее распространенных случая:

Enter image description here

+9
30 апр. '14 в 22:21
источник

Простой способ объединить их и убедиться в их правильности:

string.Format("{0}/{1}", Url1.Trim('/'), Url2);
+8
12 мая '10 в 21:42
источник

Я обнаружил, что UriBuilder действительно хорошо работает для такого рода вещей:

UriBuilder urlb = new UriBuilder("http", _serverAddress, _webPort, _filePath);
Uri url = urlb.Uri;
return url.AbsoluteUri;

Посмотрите Класс UriBuilder - MSDN для большего количества конструкторов и документации.

+6
22 мая '13 в 12:19
источник

Здесь метод Microsoft (OfficeDev PnP) UrlUtility.Combine:

    const char PATH_DELIMITER = '/';

    /// <summary>
    /// Combines a path and a relative path.
    /// </summary>
    /// <param name="path"></param>
    /// <param name="relative"></param>
    /// <returns></returns>
    public static string Combine(string path, string relative) 
    {
        if(relative == null)
            relative = String.Empty;

        if(path == null)
            path = String.Empty;

        if(relative.Length == 0 && path.Length == 0)
            return String.Empty;

        if(relative.Length == 0)
            return path;

        if(path.Length == 0)
            return relative;

        path = path.Replace('\\', PATH_DELIMITER);
        relative = relative.Replace('\\', PATH_DELIMITER);

        return path.TrimEnd(PATH_DELIMITER) + PATH_DELIMITER + relative.TrimStart(PATH_DELIMITER);
    }

Источник: GitHub

+4
01 нояб. '15 в 18:34
источник

Мое общее решение:

public static string Combine(params string[] uriParts)
{
    string uri = string.Empty;
    if (uriParts != null && uriParts.Any())
    {
        char[] trims = new char[] { '\\', '/' };
        uri = (uriParts[0] ?? string.Empty).TrimEnd(trims);

        for (int i = 1; i < uriParts.Length; i++)
        {
            uri = string.Format("{0}/{1}", uri.TrimEnd(trims), (uriParts[i] ?? string.Empty).TrimStart(trims));
        }
    }

    return uri;
}
+3
17 мая '15 в 7:52
источник

Я создал эту функцию, которая сделает вашу жизнь проще:

    /// <summary>
    /// The ultimate Path combiner of all time
    /// </summary>
    /// <param name="IsURL">
    /// true - if the paths are Internet URLs, false - if the paths are local URLs, this is very important as this will be used to decide which separator will be used.
    /// </param>
    /// <param name="IsRelative">Just adds the separator at the beginning</param>
    /// <param name="IsFixInternal">Fix the paths from within (by removing duplicate separators and correcting the separators)</param>
    /// <param name="parts">The paths to combine</param>
    /// <returns>the combined path</returns>
    public static string PathCombine(bool IsURL , bool IsRelative , bool IsFixInternal , params string[] parts)
    {
        if (parts == null || parts.Length == 0) return string.Empty;
        char separator = IsURL ? '/' : '\\';

        if (parts.Length == 1 && IsFixInternal)
        {
            string validsingle;
            if (IsURL)
            {
                validsingle = parts[0].Replace('\\' , '/');
            }
            else
            {
                validsingle = parts[0].Replace('/' , '\\');
            }
            validsingle = validsingle.Trim(separator);
            return (IsRelative ? separator.ToString() : string.Empty) + validsingle;
        }

        string final = parts
            .Aggregate
            (
            (string first , string second) =>
            {
                string validfirst;
                string validsecond;
                if (IsURL)
                {
                    validfirst = first.Replace('\\' , '/');
                    validsecond = second.Replace('\\' , '/');
                }
                else
                {
                    validfirst = first.Replace('/' , '\\');
                    validsecond = second.Replace('/' , '\\');
                }
                var prefix = string.Empty;
                if (IsFixInternal)
                {
                    if (IsURL)
                    {
                        if (validfirst.Contains("://"))
                        {
                            var tofix = validfirst.Substring(validfirst.IndexOf("://") + 3);
                            prefix = validfirst.Replace(tofix , string.Empty).TrimStart(separator);

                            var tofixlist = tofix.Split(new[] { separator } , StringSplitOptions.RemoveEmptyEntries);

                            validfirst = separator + string.Join(separator.ToString() , tofixlist);
                        }
                        else
                        {
                            var firstlist = validfirst.Split(new[] { separator } , StringSplitOptions.RemoveEmptyEntries);
                            validfirst = string.Join(separator.ToString() , firstlist);
                        }

                        var secondlist = validsecond.Split(new[] { separator } , StringSplitOptions.RemoveEmptyEntries);
                        validsecond = string.Join(separator.ToString() , secondlist);
                    }
                    else
                    {
                        var firstlist = validfirst.Split(new[] { separator } , StringSplitOptions.RemoveEmptyEntries);
                        var secondlist = validsecond.Split(new[] { separator } , StringSplitOptions.RemoveEmptyEntries);

                        validfirst = string.Join(separator.ToString() , firstlist);
                        validsecond = string.Join(separator.ToString() , secondlist);
                    }
                }
                return prefix + validfirst.Trim(separator) + separator + validsecond.Trim(separator);
            }
            );
        return (IsRelative ? separator.ToString() : string.Empty) + final;
    }

Он работает как для URL, так и для обычных путей.

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

    // Fixes internal paths
    Console.WriteLine(PathCombine(true , true , true , @"\/\/folder 1\/\/\/\\/\folder2\///folder3\\/" , @"/\somefile.ext\/\//\"));
    // Result: /folder 1/folder2/folder3/somefile.ext

    // Doesn't fix internal paths
    Console.WriteLine(PathCombine(true , true , false , @"\/\/folder 1\/\/\/\\/\folder2\///folder3\\/" , @"/\somefile.ext\/\//\"));
    //result : /folder 1//////////folder2////folder3/somefile.ext

    // Don't worry about URL prefixes when fixing internal paths
    Console.WriteLine(PathCombine(true , false , true , @"/\/\/https:/\/\/\lul.com\/\/\/\\/\folder2\///folder3\\/" , @"/\somefile.ext\/\//\"));
    // Result: https://lul.com/folder2/folder3/somefile.ext

    Console.WriteLine(PathCombine(false , true , true , @"../../../\\..\...\./../somepath" , @"anotherpath"));
    // Result: \..\..\..\..\...\.\..\somepath\anotherpath
+3
21 июл. '16 в 18:19
источник

Я считаю следующее полезным и имеет следующие особенности:

  • Броски на ноль или пробел
  • Принимает params нескольких параметров для нескольких сегментов URL
  • бросает на ноль или пусто

Учебный класс

public static class UrlPath
{
   private static string InternalCombine(string source, string dest)
   {
      if (string.IsNullOrWhiteSpace(source))
         throw new ArgumentException("Cannot be null or white space", nameof(source));

      if (string.IsNullOrWhiteSpace(dest))
         throw new ArgumentException("Cannot be null or white space", nameof(dest));

      return $"{source.TrimEnd('/', '\\')}/{dest.TrimStart('/', '\\')}";
   }

   public static string Combine(string source, params string[] args) 
       => args.Aggregate(source, InternalCombine);
}

тесты

UrlPath.Combine("test1", "test2");
UrlPath.Combine("test1//", "test2");
UrlPath.Combine("test1", "/test2");

// Result = test1/test2

UrlPath.Combine(@"test1\/\/\/", @"\/\/\\\\\//test2", @"\/\/\\\\\//test3\") ;

// Result = test1/test2/test3

UrlPath.Combine("/test1/", "/test2/", null);
UrlPath.Combine("", "/test2/");
UrlPath.Combine("/test1/", null);

// Throws an ArgumentException
+3
28 февр. '17 в 2:11
источник

Правила при объединении URL с URI

Чтобы избежать странного поведения, есть одно правило:

  • Путь (каталог) должен заканчиваться на '/'. Если путь заканчивается без '/', последняя часть обрабатывается как имя файла и будет объединена при попытке объединить со следующей частью URL.
  • Есть одно исключение: базовый URL-адрес (без информации о каталоге) не должен заканчиваться на '/'
  • часть пути не должна начинаться с '/'. Если он начинается с '/', string.Empty существующая относительная информация из URL удаляется... добавление string.Empty Путь к string.Empty части также удалит относительный каталог из URL!

Если вы следуете правилам выше, вы можете комбинировать URL с кодом ниже. В зависимости от вашей ситуации, вы можете добавить несколько частей "каталога" в URL...

        var pathParts = new string[] { destinationBaseUrl, destinationFolderUrl, fileName };

        var destination = pathParts.Aggregate((left, right) =>
        {
            if (string.IsNullOrWhiteSpace(right))
                return left;

            return new Uri(new Uri(left), right).ToString();
        });
+2
05 апр. '16 в 5:04
источник

Использовать этот:

public static class WebPath
{
    public static string Combine(params string[] args)
    {
        var prefixAdjusted = args.Select(x => x.StartsWith("/") && !x.StartsWith("http") ? x.Substring(1) : x);
        return string.Join("/", prefixAdjusted);
    }
}
+1
10 дек. '12 в 15:31
источник

Вот мой подход, и я буду использовать его и для себя:

public static string UrlCombine(string part1, string part2)
{
    string newPart1 = string.Empty;
    string newPart2 = string.Empty;
    string seperator = "/";

    // If either part1 or part 2 is empty,
    // we don't need to combine with seperator
    if (string.IsNullOrEmpty(part1) || string.IsNullOrEmpty(part2))
    {
        seperator = string.Empty;
    }

    // If part1 is not empty,
    // remove '/' at last
    if (!string.IsNullOrEmpty(part1))
    {
        newPart1 = part1.TrimEnd('/');
    }

    // If part2 is not empty,
    // remove '/' at first
    if (!string.IsNullOrEmpty(part2))
    {
        newPart2 = part2.TrimStart('/');
    }

    // Now finally combine
    return string.Format("{0}{1}{2}", newPart1, seperator, newPart2);
}
+1
03 авг. '13 в 3:39
источник

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

    private Uri UriCombine(string path1, string path2, string path3 = "", string path4 = "")
    {
        string path = System.IO.Path.Combine(path1, path2.TrimStart('\\', '/'), path3.TrimStart('\\', '/'), path4.TrimStart('\\', '/'));
        string url = path.Replace('\\','/');
        return new Uri(url);
    }

Преимущество в том, что он ведет себя точно так же, как Path.Combine.

+1
18 февр. '14 в 15:31
источник

Почему бы просто не использовать следующее.

System.IO.Path.Combine(rootUrl, subPath).Replace(@"\", "/")
+1
17 нояб. '17 в 13:11
источник

Я обнаружил, что конструктор Uri переворачивает '\' на '/'. Таким образом, вы также можете использовать Path.Combine с конструктором Uri.

 Uri baseUri = new Uri("http://MyUrl.com");
 string path = Path.Combine("Images", "Image.jpg");
 Uri myUri = new Uri(baseUri, path);
+1
30 сент. '18 в 10:43
источник

Для чего это стоит, здесь пара методов расширения. Первый объединяет пути, а второй добавляет параметры в URL.

    public static string CombineUrl(this string root, string path, params string[] paths)
    {
        if (string.IsNullOrWhiteSpace(path))
        {
            return root;
        }

        Uri baseUri = new Uri(root);
        Uri combinedPaths = new Uri(baseUri, path);

        foreach (string extendedPath in paths)
        {
           combinedPaths = new Uri(combinedPaths, extendedPath);
        }

        return combinedPaths.AbsoluteUri;
    }

    public static string AddUrlParams(this string url, Dictionary<string, string> parameters)
    {
        if (parameters == null || !parameters.Keys.Any())
        {
            return url;
        }

        var tempUrl = new StringBuilder($"{url}?");
        int count = 0;

        foreach (KeyValuePair<string, string> parameter in parameters)
        {
            if (count > 0)
            {
                tempUrl.Append("&");
            }

            tempUrl.Append($"{WebUtility.UrlEncode(parameter.Key)}={WebUtility.UrlEncode(parameter.Value)}");
            count++;
        }

        return tempUrl.ToString();
    }
+1
25 мар. '19 в 17:29
источник

Я объединил все предыдущие ответы:

    public static string UrlPathCombine(string path1, string path2)
    {
        path1 = path1.TrimEnd('/') + "/";
        path2 = path2.TrimStart('/');

        return Path.Combine(path1, path2)
            .Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
    }

    [TestMethod]
    public void TestUrl()
    {
        const string P1 = "http://msdn.microsoft.com/slash/library//";
        Assert.AreEqual("http://msdn.microsoft.com/slash/library/site.aspx", UrlPathCombine(P1, "//site.aspx"));

        var path = UrlPathCombine("Http://MyUrl.com/", "Images/Image.jpg");

        Assert.AreEqual(
            "Http://MyUrl.com/Images/Image.jpg",
            path);
    }
0
25 апр. '13 в 10:51
источник

Ну, я просто объединяю две строки и использую регулярные выражения для очистки.

    public class UriTool
    {
        public static Uri Join(string path1, string path2)
        {
            string url = path1 + "/" + path2;
            url = Regex.Replace(url, "(?<!http:)/{2,}", "/");

            return new Uri(url);
        }
    }

Итак, вы можете использовать это так:

    string path1 = "http://someaddress.com/something/";
    string path2 = "/another/address.html";
    Uri joinedUri = UriTool.Join(path1, path2);

    // joinedUri.ToString() returns "http://someaddress.com/something/another/address.html"
0
15 мая '14 в 23:49
источник

Я использовал этот код для решения проблемы:

string[] brokenBaseUrl = Context.Url.TrimEnd('/').Split('/');
string[] brokenRootFolderPath = RootFolderPath.Split('/');

for (int x = 0; x < brokenRootFolderPath.Length; x++)
{
    //if url doesn't already contain member, append it to the end of the string with / in front
    if (!brokenBaseUrl.Contains(brokenRootFolderPath[x]))
    {
        if (x == 0)
        {
            RootLocationUrl = Context.Url.TrimEnd('/');
        }
        else
        {
            RootLocationUrl += String.Format("/{0}", brokenRootFolderPath[x]);
        }
    }
}
0
03 дек. '15 в 23:17
источник

Обе эти работы:

  Uri final = new Uri(Regex.Replace(baseUrl + "/" + relativePath, "(?<!http:)/{2,}", "/"));

Или же

  Uri final =new Uri(string.Format("{0}/{1}", baseUrl.ToString().TrimEnd('/'), relativePath.ToString().TrimStart('/')));

Т.е. если

baseUrl = "http://tesrurl.test.com/Int18"

а также

relativePath = "To_Folder"

output = http://tesrurl.test.com/Int18/To_Folder

Некоторые ошибки появятся для кода ниже:

 // If you use the below code, some issues will be there in the final URI
 Uri final = new Uri(baseUrl, relativePath);
0
24 апр. '17 в 7:43
источник

Простой один вкладыш:

public static string Combine(this string uri1, string uri2) => $"{uri1.TrimEnd('/')}/{uri2.TrimStart('/')}";

Вдохновленный ответом @Matt Sharpe.

0
06 нояб. '17 в 12:41
источник
  • 1
  • 2

Посмотрите другие вопросы по меткам или Задайте вопрос