wasDAVClient – Blame information for rev 2
?pathlinks?
Rev | Author | Line No. | Line |
---|---|---|---|
1 | office | 1 | using System; |
2 | using System.Collections.Generic; |
||
3 | using System.IO; |
||
4 | using System.Linq; |
||
5 | using System.Net; |
||
6 | using System.Net.Http; |
||
7 | using System.Net.Http.Headers; |
||
8 | using System.Text; |
||
9 | using System.Threading.Tasks; |
||
10 | using wasDAVClient.Helpers; |
||
11 | using wasDAVClient.Model; |
||
12 | |||
13 | namespace wasDAVClient |
||
14 | { |
||
15 | public class Client : IClient |
||
16 | { |
||
17 | private static readonly HttpMethod PropFind = new HttpMethod("PROPFIND"); |
||
18 | private static readonly HttpMethod MoveMethod = new HttpMethod("MOVE"); |
||
19 | |||
20 | private static readonly HttpMethod MkCol = new HttpMethod(WebRequestMethods.Http.MkCol); |
||
21 | |||
22 | private const int HttpStatusCode_MultiStatus = 207; |
||
23 | |||
24 | // http://webdav.org/specs/rfc4918.html#METHOD_PROPFIND |
||
25 | private const string PropFindRequestContent = |
||
26 | "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" + |
||
27 | "<propfind xmlns=\"DAV:\">" + |
||
28 | "<allprop/>" + |
||
29 | //" <propname/>" + |
||
30 | //" <prop>" + |
||
31 | //" <creationdate/>" + |
||
32 | //" <getlastmodified/>" + |
||
33 | //" <displayname/>" + |
||
34 | //" <getcontentlength/>" + |
||
35 | //" <getcontenttype/>" + |
||
36 | //" <getetag/>" + |
||
37 | //" <resourcetype/>" + |
||
38 | //" </prop> " + |
||
39 | "</propfind>"; |
||
40 | |||
41 | private static readonly string AssemblyVersion = typeof(IClient).Assembly.GetName().Version.ToString(); |
||
42 | |||
43 | private readonly HttpClient _client; |
||
44 | private readonly HttpClient _uploadClient; |
||
45 | private string _server; |
||
46 | private string _basePath = "/"; |
||
47 | |||
48 | private string _encodedBasePath; |
||
49 | |||
50 | |||
51 | |||
52 | #region WebDAV connection parameters |
||
53 | |||
54 | /// <summary> |
||
55 | /// Specify the WebDAV hostname (required). |
||
56 | /// </summary> |
||
57 | public string Server |
||
58 | { |
||
59 | get |
||
60 | { |
||
61 | return _server; |
||
62 | } |
||
63 | set |
||
64 | { |
||
65 | value = value.TrimEnd('/'); |
||
66 | _server = value; |
||
67 | } |
||
68 | } |
||
69 | |||
70 | /// <summary> |
||
71 | /// Specify the path of a WebDAV directory to use as 'root' (default: /) |
||
72 | /// </summary> |
||
73 | public string BasePath |
||
74 | { |
||
75 | get |
||
76 | { |
||
77 | return _basePath; |
||
78 | } |
||
79 | set |
||
80 | { |
||
81 | value = value.Trim('/'); |
||
82 | if (string.IsNullOrEmpty(value)) |
||
83 | _basePath = "/"; |
||
84 | else |
||
85 | _basePath = "/" + value + "/"; |
||
86 | } |
||
87 | } |
||
88 | |||
89 | /// <summary> |
||
90 | /// Specify an port (default: null = auto-detect) |
||
91 | /// </summary> |
||
92 | public int? Port |
||
93 | { |
||
94 | get; set; |
||
95 | } |
||
96 | |||
97 | /// <summary> |
||
98 | /// Specify the UserAgent (and UserAgent version) string to use in requests |
||
99 | /// </summary> |
||
100 | public string UserAgent |
||
101 | { |
||
102 | get; set; |
||
103 | } |
||
104 | |||
105 | /// <summary> |
||
106 | /// Specify the UserAgent (and UserAgent version) string to use in requests |
||
107 | /// </summary> |
||
108 | public string UserAgentVersion |
||
109 | { |
||
110 | get; set; |
||
111 | } |
||
112 | |||
113 | #endregion |
||
114 | |||
115 | |||
2 | office | 116 | public Client(NetworkCredential credential = null, TimeSpan? timeout = null, IWebProxy proxy = null) |
1 | office | 117 | { |
118 | var handler = new HttpClientHandler(); |
||
119 | if (proxy != null && handler.SupportsProxy) |
||
120 | handler.Proxy = proxy; |
||
121 | if (handler.SupportsAutomaticDecompression) |
||
122 | handler.AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip; |
||
123 | if (credential != null) |
||
124 | { |
||
125 | handler.Credentials = credential; |
||
126 | handler.PreAuthenticate = true; |
||
127 | } |
||
128 | |||
129 | _client = new HttpClient(handler); |
||
130 | _client.DefaultRequestHeaders.ExpectContinue = false; |
||
131 | |||
2 | office | 132 | if (timeout != null) |
1 | office | 133 | { |
134 | _uploadClient = new HttpClient(handler); |
||
135 | _uploadClient.DefaultRequestHeaders.ExpectContinue = false; |
||
2 | office | 136 | _uploadClient.Timeout = timeout.Value; |
1 | office | 137 | } |
138 | |||
139 | } |
||
140 | |||
141 | #region WebDAV operations |
||
142 | |||
143 | /// <summary> |
||
144 | /// List all files present on the server. |
||
145 | /// </summary> |
||
146 | /// <param name="path">List only files in this path</param> |
||
147 | /// <param name="depth">Recursion depth</param> |
||
148 | /// <returns>A list of files (entries without a trailing slash) and directories (entries with a trailing slash)</returns> |
||
149 | public async Task<IEnumerable<Item>> List(string path = "/", int? depth = 1) |
||
150 | { |
||
151 | var listUri = await GetServerUrl(path, true).ConfigureAwait(false); |
||
152 | |||
153 | // Depth header: http://webdav.org/specs/rfc4918.html#rfc.section.9.1.4 |
||
154 | IDictionary<string, string> headers = new Dictionary<string, string>(); |
||
155 | if (depth != null) |
||
156 | { |
||
157 | headers.Add("Depth", depth.ToString()); |
||
158 | } |
||
159 | |||
160 | |||
161 | HttpResponseMessage response = null; |
||
162 | |||
163 | try |
||
164 | { |
||
165 | response = await HttpRequest(listUri.Uri, PropFind, headers, Encoding.UTF8.GetBytes(PropFindRequestContent)).ConfigureAwait(false); |
||
166 | |||
167 | if (response.StatusCode != HttpStatusCode.OK && |
||
168 | (int)response.StatusCode != HttpStatusCode_MultiStatus) |
||
169 | { |
||
170 | throw new wasDAVException((int)response.StatusCode, "Failed retrieving items in folder."); |
||
171 | } |
||
172 | |||
173 | using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) |
||
174 | { |
||
175 | var items = ResponseParser.ParseItems(stream); |
||
176 | |||
177 | if (items == null) |
||
178 | { |
||
179 | throw new wasDAVException("Failed deserializing data returned from server."); |
||
180 | } |
||
181 | |||
182 | var listUrl = listUri.ToString(); |
||
183 | |||
184 | var result = new List<Item>(items.Count()); |
||
185 | foreach (var item in items) |
||
186 | { |
||
187 | // If it's not a collection, add it to the result |
||
188 | if (!item.IsCollection) |
||
189 | { |
||
190 | result.Add(item); |
||
191 | } |
||
192 | else |
||
193 | { |
||
194 | // If it's not the requested parent folder, add it to the result |
||
195 | var fullHref = await GetServerUrl(item.Href, true).ConfigureAwait(false); |
||
196 | if (!string.Equals(fullHref.ToString(), listUrl, StringComparison.CurrentCultureIgnoreCase)) |
||
197 | { |
||
198 | result.Add(item); |
||
199 | } |
||
200 | } |
||
201 | } |
||
202 | return result; |
||
203 | } |
||
204 | |||
205 | } |
||
206 | finally |
||
207 | { |
||
208 | if (response != null) |
||
209 | response.Dispose(); |
||
210 | } |
||
211 | } |
||
212 | |||
213 | /// <summary> |
||
214 | /// List all files present on the server. |
||
215 | /// </summary> |
||
216 | /// <returns>A list of files (entries without a trailing slash) and directories (entries with a trailing slash)</returns> |
||
217 | public async Task<Item> GetFolder(string path = "/") |
||
218 | { |
||
219 | var listUri = await GetServerUrl(path, true).ConfigureAwait(false); |
||
220 | return await Get(listUri.Uri, path).ConfigureAwait(false); |
||
221 | } |
||
222 | |||
223 | /// <summary> |
||
224 | /// List all files present on the server. |
||
225 | /// </summary> |
||
226 | /// <returns>A list of files (entries without a trailing slash) and directories (entries with a trailing slash)</returns> |
||
227 | public async Task<Item> GetFile(string path = "/") |
||
228 | { |
||
229 | var listUri = await GetServerUrl(path, false).ConfigureAwait(false); |
||
230 | return await Get(listUri.Uri, path).ConfigureAwait(false); |
||
231 | } |
||
232 | |||
233 | |||
234 | /// <summary> |
||
235 | /// List all files present on the server. |
||
236 | /// </summary> |
||
237 | /// <returns>A list of files (entries without a trailing slash) and directories (entries with a trailing slash)</returns> |
||
238 | private async Task<Item> Get(Uri listUri, string path) |
||
239 | { |
||
240 | |||
241 | // Depth header: http://webdav.org/specs/rfc4918.html#rfc.section.9.1.4 |
||
242 | IDictionary<string, string> headers = new Dictionary<string, string>(); |
||
243 | headers.Add("Depth", "0"); |
||
244 | |||
245 | |||
246 | HttpResponseMessage response = null; |
||
247 | |||
248 | try |
||
249 | { |
||
250 | response = await HttpRequest(listUri, PropFind, headers, Encoding.UTF8.GetBytes(PropFindRequestContent)).ConfigureAwait(false); |
||
251 | |||
252 | if (response.StatusCode != HttpStatusCode.OK && |
||
253 | (int)response.StatusCode != HttpStatusCode_MultiStatus) |
||
254 | { |
||
255 | throw new wasDAVException((int)response.StatusCode, string.Format("Failed retrieving item/folder (Status Code: {0})", response.StatusCode)); |
||
256 | } |
||
257 | |||
258 | using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) |
||
259 | { |
||
260 | var result = ResponseParser.ParseItem(stream); |
||
261 | |||
262 | if (result == null) |
||
263 | { |
||
264 | throw new wasDAVException("Failed deserializing data returned from server."); |
||
265 | } |
||
266 | |||
267 | return result; |
||
268 | } |
||
269 | } |
||
270 | finally |
||
271 | { |
||
272 | if (response != null) |
||
273 | response.Dispose(); |
||
274 | } |
||
275 | } |
||
276 | |||
277 | /// <summary> |
||
278 | /// Download a file from the server |
||
279 | /// </summary> |
||
280 | /// <param name="remoteFilePath">Source path and filename of the file on the server</param> |
||
281 | public async Task<Stream> Download(string remoteFilePath) |
||
282 | { |
||
283 | // Should not have a trailing slash. |
||
284 | var downloadUri = await GetServerUrl(remoteFilePath, false).ConfigureAwait(false); |
||
285 | |||
286 | var dictionary = new Dictionary<string, string> { { "translate", "f" } }; |
||
287 | var response = await HttpRequest(downloadUri.Uri, HttpMethod.Get, dictionary).ConfigureAwait(false); |
||
288 | if (response.StatusCode != HttpStatusCode.OK) |
||
289 | { |
||
290 | throw new wasDAVException((int)response.StatusCode, "Failed retrieving file."); |
||
291 | } |
||
292 | return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); |
||
293 | } |
||
294 | |||
295 | /// <summary> |
||
296 | /// Download a file from the server |
||
297 | /// </summary> |
||
298 | /// <param name="remoteFilePath">Source path and filename of the file on the server</param> |
||
299 | /// <param name="content"></param> |
||
300 | /// <param name="name"></param> |
||
301 | public async Task<bool> Upload(string remoteFilePath, Stream content, string name) |
||
302 | { |
||
303 | // Should not have a trailing slash. |
||
304 | var uploadUri = await GetServerUrl(remoteFilePath.TrimEnd('/') + "/" + name.TrimStart('/'), false).ConfigureAwait(false); |
||
305 | |||
306 | HttpResponseMessage response = null; |
||
307 | |||
308 | try |
||
309 | { |
||
310 | response = await HttpUploadRequest(uploadUri.Uri, HttpMethod.Put, content).ConfigureAwait(false); |
||
311 | |||
312 | if (response.StatusCode != HttpStatusCode.OK && |
||
313 | response.StatusCode != HttpStatusCode.NoContent && |
||
314 | response.StatusCode != HttpStatusCode.Created) |
||
315 | { |
||
316 | throw new wasDAVException((int)response.StatusCode, "Failed uploading file."); |
||
317 | } |
||
318 | |||
319 | return response.IsSuccessStatusCode; |
||
320 | } |
||
321 | finally |
||
322 | { |
||
323 | if (response != null) |
||
324 | response.Dispose(); |
||
325 | } |
||
326 | |||
327 | } |
||
328 | |||
329 | |||
330 | /// <summary> |
||
331 | /// Create a directory on the server |
||
332 | /// </summary> |
||
333 | /// <param name="remotePath">Destination path of the directory on the server</param> |
||
334 | /// <param name="name"></param> |
||
335 | public async Task<bool> CreateDir(string remotePath, string name) |
||
336 | { |
||
337 | // Should not have a trailing slash. |
||
338 | var dirUri = await GetServerUrl(remotePath.TrimEnd('/') + "/" + name.TrimStart('/'), false).ConfigureAwait(false); |
||
339 | |||
340 | HttpResponseMessage response = null; |
||
341 | |||
342 | try |
||
343 | { |
||
344 | response = await HttpRequest(dirUri.Uri, MkCol).ConfigureAwait(false); |
||
345 | |||
346 | if (response.StatusCode == HttpStatusCode.Conflict) |
||
347 | throw new wasDAVConflictException((int)response.StatusCode, "Failed creating folder."); |
||
348 | |||
349 | if (response.StatusCode != HttpStatusCode.OK && |
||
350 | response.StatusCode != HttpStatusCode.NoContent && |
||
351 | response.StatusCode != HttpStatusCode.Created) |
||
352 | { |
||
353 | throw new wasDAVException((int)response.StatusCode, "Failed creating folder."); |
||
354 | } |
||
355 | |||
356 | return response.IsSuccessStatusCode; |
||
357 | } |
||
358 | finally |
||
359 | { |
||
360 | if (response != null) |
||
361 | response.Dispose(); |
||
362 | } |
||
363 | } |
||
364 | |||
365 | public async Task DeleteFolder(string href) |
||
366 | { |
||
367 | var listUri = await GetServerUrl(href, true).ConfigureAwait(false); |
||
368 | await Delete(listUri.Uri).ConfigureAwait(false); |
||
369 | } |
||
370 | |||
371 | public async Task DeleteFile(string href) |
||
372 | { |
||
373 | var listUri = await GetServerUrl(href, false).ConfigureAwait(false); |
||
374 | await Delete(listUri.Uri).ConfigureAwait(false); |
||
375 | } |
||
376 | |||
377 | |||
378 | private async Task Delete(Uri listUri) |
||
379 | { |
||
380 | var response = await HttpRequest(listUri, HttpMethod.Delete).ConfigureAwait(false); |
||
381 | |||
382 | if (response.StatusCode != HttpStatusCode.OK && |
||
383 | response.StatusCode != HttpStatusCode.NoContent) |
||
384 | { |
||
385 | throw new wasDAVException((int)response.StatusCode, "Failed deleting item."); |
||
386 | } |
||
387 | } |
||
388 | |||
389 | public async Task<bool> MoveFolder(string srcFolderPath, string dstFolderPath) |
||
390 | { |
||
391 | // Should have a trailing slash. |
||
392 | var srcUri = await GetServerUrl(srcFolderPath, true).ConfigureAwait(false); |
||
393 | var dstUri = await GetServerUrl(dstFolderPath, true).ConfigureAwait(false); |
||
394 | |||
395 | return await Move(srcUri.Uri, dstUri.Uri).ConfigureAwait(false); |
||
396 | |||
397 | } |
||
398 | |||
399 | public async Task<bool> MoveFile(string srcFilePath, string dstFilePath) |
||
400 | { |
||
401 | // Should not have a trailing slash. |
||
402 | var srcUri = await GetServerUrl(srcFilePath, false).ConfigureAwait(false); |
||
403 | var dstUri = await GetServerUrl(dstFilePath, false).ConfigureAwait(false); |
||
404 | |||
405 | return await Move(srcUri.Uri, dstUri.Uri).ConfigureAwait(false); |
||
406 | } |
||
407 | |||
408 | |||
409 | private async Task<bool> Move(Uri srcUri, Uri dstUri) |
||
410 | { |
||
411 | const string requestContent = "MOVE"; |
||
412 | |||
413 | IDictionary<string, string> headers = new Dictionary<string, string>(); |
||
414 | headers.Add("Destination", dstUri.ToString()); |
||
415 | |||
416 | var response = await HttpRequest(srcUri, MoveMethod, headers, Encoding.UTF8.GetBytes(requestContent)).ConfigureAwait(false); |
||
417 | |||
418 | if (response.StatusCode != HttpStatusCode.OK && |
||
419 | response.StatusCode != HttpStatusCode.Created) |
||
420 | { |
||
421 | throw new wasDAVException((int)response.StatusCode, "Failed moving file."); |
||
422 | } |
||
423 | |||
424 | return response.IsSuccessStatusCode; |
||
425 | } |
||
426 | |||
427 | #endregion |
||
428 | |||
429 | #region Server communication |
||
430 | |||
431 | /// <summary> |
||
432 | /// Perform the WebDAV call and fire the callback when finished. |
||
433 | /// </summary> |
||
434 | /// <param name="uri"></param> |
||
435 | /// <param name="method"></param> |
||
436 | /// <param name="headers"></param> |
||
437 | /// <param name="content"></param> |
||
438 | private async Task<HttpResponseMessage> HttpRequest(Uri uri, HttpMethod method, IDictionary<string, string> headers = null, byte[] content = null) |
||
439 | { |
||
440 | using (var request = new HttpRequestMessage(method, uri)) |
||
441 | { |
||
442 | request.Headers.Connection.Add("Keep-Alive"); |
||
443 | if (!string.IsNullOrWhiteSpace(UserAgent)) |
||
444 | request.Headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent, UserAgentVersion)); |
||
445 | else |
||
446 | request.Headers.UserAgent.Add(new ProductInfoHeaderValue("WebDAVClient", AssemblyVersion)); |
||
447 | |||
448 | if (headers != null) |
||
449 | { |
||
450 | foreach (string key in headers.Keys) |
||
451 | { |
||
452 | request.Headers.Add(key, headers[key]); |
||
453 | } |
||
454 | } |
||
455 | |||
456 | // Need to send along content? |
||
457 | if (content != null) |
||
458 | { |
||
459 | request.Content = new ByteArrayContent(content); |
||
460 | request.Content.Headers.ContentType = new MediaTypeHeaderValue("text/xml"); |
||
461 | } |
||
462 | |||
463 | return await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); |
||
464 | } |
||
465 | } |
||
466 | |||
467 | /// <summary> |
||
468 | /// Perform the WebDAV call and fire the callback when finished. |
||
469 | /// </summary> |
||
470 | /// <param name="uri"></param> |
||
471 | /// <param name="headers"></param> |
||
472 | /// <param name="method"></param> |
||
473 | /// <param name="content"></param> |
||
474 | private async Task<HttpResponseMessage> HttpUploadRequest(Uri uri, HttpMethod method, Stream content, IDictionary<string, string> headers = null) |
||
475 | { |
||
476 | using (var request = new HttpRequestMessage(method, uri)) |
||
477 | { |
||
478 | request.Headers.Connection.Add("Keep-Alive"); |
||
479 | if (!string.IsNullOrWhiteSpace(UserAgent)) |
||
480 | request.Headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent, UserAgentVersion)); |
||
481 | else |
||
482 | request.Headers.UserAgent.Add(new ProductInfoHeaderValue("WebDAVClient", AssemblyVersion)); |
||
483 | |||
484 | if (headers != null) |
||
485 | { |
||
486 | foreach (string key in headers.Keys) |
||
487 | { |
||
488 | request.Headers.Add(key, headers[key]); |
||
489 | } |
||
490 | } |
||
491 | |||
492 | // Need to send along content? |
||
493 | if (content != null) |
||
494 | { |
||
495 | request.Content = new StreamContent(content); |
||
496 | } |
||
497 | |||
498 | var client = _uploadClient ?? _client; |
||
499 | return await client.SendAsync(request).ConfigureAwait(false); |
||
500 | } |
||
501 | } |
||
502 | |||
503 | /// <summary> |
||
504 | /// Try to create an Uri with kind UriKind.Absolute |
||
505 | /// This particular implementation also works on Mono/Linux |
||
506 | /// It seems that on Mono it is expected behaviour that uris |
||
507 | /// of kind /a/b are indeed absolute uris since it referes to a file in /a/b. |
||
508 | /// https://bugzilla.xamarin.com/show_bug.cgi?id=30854 |
||
509 | /// </summary> |
||
510 | /// <param name="uriString"></param> |
||
511 | /// <param name="uriResult"></param> |
||
512 | /// <returns></returns> |
||
513 | private static bool TryCreateAbsolute(string uriString, out Uri uriResult) |
||
514 | { |
||
515 | return Uri.TryCreate(uriString, UriKind.Absolute, out uriResult) && uriResult.Scheme != Uri.UriSchemeFile; |
||
516 | } |
||
517 | |||
518 | private async Task<UriBuilder> GetServerUrl(string path, bool appendTrailingSlash) |
||
519 | { |
||
520 | // Resolve the base path on the server |
||
521 | if (_encodedBasePath == null) |
||
522 | { |
||
523 | var baseUri = new UriBuilder(_server) { Path = _basePath }; |
||
524 | var root = await Get(baseUri.Uri, null).ConfigureAwait(false); |
||
525 | |||
526 | _encodedBasePath = root.Href; |
||
527 | } |
||
528 | |||
529 | |||
530 | // If we've been asked for the "root" folder |
||
531 | if (string.IsNullOrEmpty(path)) |
||
532 | { |
||
533 | // If the resolved base path is an absolute URI, use it |
||
534 | Uri absoluteBaseUri; |
||
535 | if (TryCreateAbsolute(_encodedBasePath, out absoluteBaseUri)) |
||
536 | { |
||
537 | return new UriBuilder(absoluteBaseUri); |
||
538 | } |
||
539 | |||
540 | // Otherwise, use the resolved base path relatively to the server |
||
541 | var baseUri = new UriBuilder(_server) { Path = _encodedBasePath }; |
||
542 | return baseUri; |
||
543 | } |
||
544 | |||
545 | // If the requested path is absolute, use it |
||
546 | Uri absoluteUri; |
||
547 | if (TryCreateAbsolute(path, out absoluteUri)) |
||
548 | { |
||
549 | var baseUri = new UriBuilder(absoluteUri); |
||
550 | return baseUri; |
||
551 | } |
||
552 | else |
||
553 | { |
||
554 | // Otherwise, create a URI relative to the server |
||
555 | UriBuilder baseUri; |
||
556 | if (TryCreateAbsolute(_encodedBasePath, out absoluteUri)) |
||
557 | { |
||
558 | baseUri = new UriBuilder(absoluteUri); |
||
559 | |||
560 | baseUri.Path = baseUri.Path.TrimEnd('/') + "/" + path.TrimStart('/'); |
||
561 | |||
562 | if (appendTrailingSlash && !baseUri.Path.EndsWith("/")) |
||
563 | baseUri.Path += "/"; |
||
564 | } |
||
565 | else |
||
566 | { |
||
567 | baseUri = new UriBuilder(_server); |
||
568 | |||
569 | // Ensure we don't add the base path twice |
||
570 | var finalPath = path; |
||
571 | if (!finalPath.StartsWith(_encodedBasePath, StringComparison.InvariantCultureIgnoreCase)) |
||
572 | { |
||
573 | finalPath = _encodedBasePath.TrimEnd('/') + "/" + path; |
||
574 | } |
||
575 | if (appendTrailingSlash) |
||
576 | finalPath = finalPath.TrimEnd('/') + "/"; |
||
577 | |||
578 | baseUri.Path = finalPath; |
||
579 | } |
||
580 | |||
581 | |||
582 | return baseUri; |
||
583 | } |
||
584 | } |
||
585 | |||
586 | #endregion |
||
587 | } |
||
588 | } |