aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/OpenSim/Services/Connectors/Asset/AssetServicesConnector.cs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--OpenSim/Services/Connectors/Asset/AssetServicesConnector.cs458
1 files changed, 369 insertions, 89 deletions
diff --git a/OpenSim/Services/Connectors/Asset/AssetServicesConnector.cs b/OpenSim/Services/Connectors/Asset/AssetServicesConnector.cs
index bd43552..7e81be7 100644
--- a/OpenSim/Services/Connectors/Asset/AssetServicesConnector.cs
+++ b/OpenSim/Services/Connectors/Asset/AssetServicesConnector.cs
@@ -27,12 +27,14 @@
27 27
28using log4net; 28using log4net;
29using System; 29using System;
30using System.Threading;
30using System.Collections.Generic; 31using System.Collections.Generic;
31using System.IO; 32using System.IO;
32using System.Reflection; 33using System.Reflection;
34using System.Timers;
33using Nini.Config; 35using Nini.Config;
34using OpenSim.Framework; 36using OpenSim.Framework;
35using OpenSim.Framework.Console; 37using OpenSim.Framework.Monitoring;
36using OpenSim.Services.Interfaces; 38using OpenSim.Services.Interfaces;
37using OpenMetaverse; 39using OpenMetaverse;
38 40
@@ -44,15 +46,27 @@ namespace OpenSim.Services.Connectors
44 LogManager.GetLogger( 46 LogManager.GetLogger(
45 MethodBase.GetCurrentMethod().DeclaringType); 47 MethodBase.GetCurrentMethod().DeclaringType);
46 48
49 const int MAXSENDRETRIESLEN = 30;
50
47 private string m_ServerURI = String.Empty; 51 private string m_ServerURI = String.Empty;
48 private IImprovedAssetCache m_Cache = null; 52 private IAssetCache m_Cache = null;
53 private int m_retryCounter;
54 private bool m_inRetries;
55 private List<AssetBase>[] m_sendRetries = new List<AssetBase>[MAXSENDRETRIESLEN];
56 private System.Timers.Timer m_retryTimer;
49 private int m_maxAssetRequestConcurrency = 30; 57 private int m_maxAssetRequestConcurrency = 30;
50 58
51 private delegate void AssetRetrievedEx(AssetBase asset); 59 private delegate void AssetRetrievedEx(AssetBase asset);
52 60
53 // Keeps track of concurrent requests for the same asset, so that it's only loaded once. 61 // Keeps track of concurrent requests for the same asset, so that it's only loaded once.
54 // Maps: Asset ID -> Handlers which will be called when the asset has been loaded 62 // Maps: Asset ID -> Handlers which will be called when the asset has been loaded
55 private Dictionary<string, AssetRetrievedEx> m_AssetHandlers = new Dictionary<string, AssetRetrievedEx>(); 63// private Dictionary<string, AssetRetrievedEx> m_AssetHandlers = new Dictionary<string, AssetRetrievedEx>();
64
65 private Dictionary<string, List<AssetRetrievedEx>> m_AssetHandlers = new Dictionary<string, List<AssetRetrievedEx>>();
66
67 private Dictionary<string, string> m_UriMap = new Dictionary<string, string>();
68
69 private Thread[] m_fetchThreads;
56 70
57 public int MaxAssetRequestConcurrency 71 public int MaxAssetRequestConcurrency
58 { 72 {
@@ -91,31 +105,155 @@ namespace OpenSim.Services.Connectors
91 string serviceURI = assetConfig.GetString("AssetServerURI", 105 string serviceURI = assetConfig.GetString("AssetServerURI",
92 String.Empty); 106 String.Empty);
93 107
108 m_ServerURI = serviceURI;
109
94 if (serviceURI == String.Empty) 110 if (serviceURI == String.Empty)
95 { 111 {
96 m_log.Error("[ASSET CONNECTOR]: No Server URI named in section AssetService"); 112 m_log.Error("[ASSET CONNECTOR]: No Server URI named in section AssetService");
97 throw new Exception("Asset connector init error"); 113 throw new Exception("Asset connector init error");
98 } 114 }
99 115
100 m_ServerURI = serviceURI; 116 m_retryTimer = new System.Timers.Timer();
117 m_retryTimer.Elapsed += new ElapsedEventHandler(retryCheck);
118 m_retryTimer.AutoReset = true;
119 m_retryTimer.Interval = 60000;
120
121 Uri serverUri = new Uri(m_ServerURI);
122
123 string groupHost = serverUri.Host;
124
125 for (int i = 0 ; i < 256 ; i++)
126 {
127 string prefix = i.ToString("x2");
128 groupHost = assetConfig.GetString("AssetServerHost_"+prefix, groupHost);
129
130 m_UriMap[prefix] = groupHost;
131 //m_log.DebugFormat("[ASSET]: Using {0} for prefix {1}", groupHost, prefix);
132 }
133
134 m_fetchThreads = new Thread[2];
135
136 for (int i = 0 ; i < 2 ; i++)
137 {
138 m_fetchThreads[i] = WorkManager.StartThread(AssetRequestProcessor,
139 String.Format("GetTextureWorker{0}", i),
140 ThreadPriority.Normal,
141 true,
142 false);
143 }
144 }
145
146 private string MapServer(string id)
147 {
148 if (m_UriMap.Count == 0)
149 return m_ServerURI;
150
151 UriBuilder serverUri = new UriBuilder(m_ServerURI);
152
153 string prefix = id.Substring(0, 2).ToLower();
154
155 string host;
156
157 // HG URLs will not be valid UUIDS
158 if (m_UriMap.ContainsKey(prefix))
159 host = m_UriMap[prefix];
160 else
161 host = m_UriMap["00"];
162
163 serverUri.Host = host;
164
165 // m_log.DebugFormat("[ASSET]: Using {0} for host name for prefix {1}", host, prefix);
166
167 string ret = serverUri.Uri.AbsoluteUri;
168 if (ret.EndsWith("/"))
169 ret = ret.Substring(0, ret.Length - 1);
170 return ret;
101 } 171 }
102 172
103 protected void SetCache(IImprovedAssetCache cache) 173 protected void retryCheck(object source, ElapsedEventArgs e)
174 {
175 lock(m_sendRetries)
176 {
177 if(m_inRetries)
178 return;
179 m_inRetries = true;
180 }
181
182 m_retryCounter++;
183 if(m_retryCounter >= 61 ) // avoid overflow 60 is max in use below
184 m_retryCounter = 1;
185
186 int inUse = 0;
187 int nextlevel;
188 int timefactor;
189 List<AssetBase> retrylist;
190 // we need to go down
191 for(int i = MAXSENDRETRIESLEN - 1; i >= 0; i--)
192 {
193 lock(m_sendRetries)
194 retrylist = m_sendRetries[i];
195
196 if(retrylist == null)
197 continue;
198
199 inUse++;
200 nextlevel = i + 1;
201
202 //We exponentially fall back on frequency until we reach one attempt per hour
203 //The net result is that we end up in the queue for roughly 24 hours..
204 //24 hours worth of assets could be a lot, so the hope is that the region admin
205 //will have gotten the asset connector back online quickly!
206 if(i == 0)
207 timefactor = 1;
208 else
209 {
210 timefactor = 1 << nextlevel;
211 if (timefactor > 60)
212 timefactor = 60;
213 }
214
215 if(m_retryCounter < timefactor)
216 continue; // to update inUse;
217
218 if (m_retryCounter % timefactor != 0)
219 continue;
220
221 // a list to retry
222 lock(m_sendRetries)
223 m_sendRetries[i] = null;
224
225 // we are the only ones with a copy of this retrylist now
226 foreach(AssetBase ass in retrylist)
227 retryStore(ass, nextlevel);
228 }
229
230 lock(m_sendRetries)
231 {
232 if(inUse == 0 )
233 m_retryTimer.Stop();
234
235 m_inRetries = false;
236 }
237 }
238
239 protected void SetCache(IAssetCache cache)
104 { 240 {
105 m_Cache = cache; 241 m_Cache = cache;
106 } 242 }
107 243
108 public AssetBase Get(string id) 244 public AssetBase Get(string id)
109 { 245 {
110// m_log.DebugFormat("[ASSET SERVICE CONNECTOR]: Synchronous get request for {0}", id); 246 string uri = MapServer(id) + "/assets/" + id;
111
112 string uri = m_ServerURI + "/assets/" + id;
113 247
114 AssetBase asset = null; 248 AssetBase asset = null;
249
115 if (m_Cache != null) 250 if (m_Cache != null)
116 asset = m_Cache.Get(id); 251 {
252 if (!m_Cache.Get(id, out asset))
253 return null;
254 }
117 255
118 if (asset == null) 256 if (asset == null || asset.Data == null || asset.Data.Length == 0)
119 { 257 {
120 // XXX: Commented out for now since this has either never been properly operational or not for some time 258 // XXX: Commented out for now since this has either never been properly operational or not for some time
121 // as m_maxAssetRequestConcurrency was being passed as the timeout, not a concurrency limiting option. 259 // as m_maxAssetRequestConcurrency was being passed as the timeout, not a concurrency limiting option.
@@ -128,8 +266,14 @@ namespace OpenSim.Services.Connectors
128 266
129 asset = SynchronousRestObjectRequester.MakeRequest<int, AssetBase>("GET", uri, 0, m_Auth); 267 asset = SynchronousRestObjectRequester.MakeRequest<int, AssetBase>("GET", uri, 0, m_Auth);
130 268
269
131 if (m_Cache != null) 270 if (m_Cache != null)
132 m_Cache.Cache(asset); 271 {
272 if (asset != null)
273 m_Cache.Cache(asset);
274 else
275 m_Cache.CacheNegative(id);
276 }
133 } 277 }
134 return asset; 278 return asset;
135 } 279 }
@@ -138,23 +282,28 @@ namespace OpenSim.Services.Connectors
138 { 282 {
139// m_log.DebugFormat("[ASSET SERVICE CONNECTOR]: Cache request for {0}", id); 283// m_log.DebugFormat("[ASSET SERVICE CONNECTOR]: Cache request for {0}", id);
140 284
285 AssetBase asset = null;
141 if (m_Cache != null) 286 if (m_Cache != null)
142 return m_Cache.Get(id); 287 {
288 m_Cache.Get(id, out asset);
289 }
143 290
144 return null; 291 return asset;
145 } 292 }
146 293
147 public AssetMetadata GetMetadata(string id) 294 public AssetMetadata GetMetadata(string id)
148 { 295 {
149 if (m_Cache != null) 296 if (m_Cache != null)
150 { 297 {
151 AssetBase fullAsset = m_Cache.Get(id); 298 AssetBase fullAsset;
299 if (!m_Cache.Get(id, out fullAsset))
300 return null;
152 301
153 if (fullAsset != null) 302 if (fullAsset != null)
154 return fullAsset.Metadata; 303 return fullAsset.Metadata;
155 } 304 }
156 305
157 string uri = m_ServerURI + "/assets/" + id + "/metadata"; 306 string uri = MapServer(id) + "/assets/" + id + "/metadata";
158 307
159 AssetMetadata asset = SynchronousRestObjectRequester.MakeRequest<int, AssetMetadata>("GET", uri, 0, m_Auth); 308 AssetMetadata asset = SynchronousRestObjectRequester.MakeRequest<int, AssetMetadata>("GET", uri, 0, m_Auth);
160 return asset; 309 return asset;
@@ -164,13 +313,15 @@ namespace OpenSim.Services.Connectors
164 { 313 {
165 if (m_Cache != null) 314 if (m_Cache != null)
166 { 315 {
167 AssetBase fullAsset = m_Cache.Get(id); 316 AssetBase fullAsset;
317 if (!m_Cache.Get(id, out fullAsset))
318 return null;
168 319
169 if (fullAsset != null) 320 if (fullAsset != null)
170 return fullAsset.Data; 321 return fullAsset.Data;
171 } 322 }
172 323
173 using (RestClient rc = new RestClient(m_ServerURI)) 324 using (RestClient rc = new RestClient(MapServer(id)))
174 { 325 {
175 rc.AddResourcePath("assets"); 326 rc.AddResourcePath("assets");
176 rc.AddResourcePath(id); 327 rc.AddResourcePath(id);
@@ -178,81 +329,110 @@ namespace OpenSim.Services.Connectors
178 329
179 rc.RequestMethod = "GET"; 330 rc.RequestMethod = "GET";
180 331
181 Stream s = rc.Request(m_Auth); 332 using (Stream s = rc.Request(m_Auth))
333 {
334 if (s == null)
335 return null;
182 336
183 if (s == null) 337 if (s.Length > 0)
184 return null; 338 {
339 byte[] ret = new byte[s.Length];
340 s.Read(ret, 0, (int)s.Length);
341
342 return ret;
343 }
344 }
345 return null;
346 }
347 }
185 348
186 if (s.Length > 0) 349 private class QueuedAssetRequest
350 {
351 public string uri;
352 public string id;
353 }
354
355 private OpenSim.Framework.BlockingQueue<QueuedAssetRequest> m_requestQueue =
356 new OpenSim.Framework.BlockingQueue<QueuedAssetRequest>();
357
358 private void AssetRequestProcessor()
359 {
360 QueuedAssetRequest r;
361
362 while (true)
363 {
364 r = m_requestQueue.Dequeue(4500);
365 Watchdog.UpdateThread();
366 if(r== null)
367 continue;
368 string uri = r.uri;
369 string id = r.id;
370
371 try
187 { 372 {
188 byte[] ret = new byte[s.Length]; 373 AssetBase a = SynchronousRestObjectRequester.MakeRequest<int, AssetBase>("GET", uri, 0, 30000, m_Auth);
189 s.Read(ret, 0, (int)s.Length);
190 374
191 return ret; 375 if (a != null && m_Cache != null)
192 } 376 m_Cache.Cache(a);
193 377
194 return null; 378 List<AssetRetrievedEx> handlers;
379 lock (m_AssetHandlers)
380 {
381 handlers = m_AssetHandlers[id];
382 m_AssetHandlers.Remove(id);
383 }
384
385 if(handlers != null)
386 {
387 Util.FireAndForget(x =>
388 {
389 foreach (AssetRetrievedEx h in handlers)
390 {
391 try { h.Invoke(a); }
392 catch { }
393 }
394 handlers.Clear();
395 });
396 }
397 }
398 catch { }
195 } 399 }
196 } 400 }
197 401
198 public bool Get(string id, Object sender, AssetRetrieved handler) 402 public bool Get(string id, Object sender, AssetRetrieved handler)
199 { 403 {
200// m_log.DebugFormat("[ASSET SERVICE CONNECTOR]: Potentially asynchronous get request for {0}", id); 404 string uri = MapServer(id) + "/assets/" + id;
201
202 string uri = m_ServerURI + "/assets/" + id;
203 405
204 AssetBase asset = null; 406 AssetBase asset = null;
205 if (m_Cache != null) 407 if (m_Cache != null)
206 asset = m_Cache.Get(id); 408 {
409 if (!m_Cache.Get(id, out asset))
410 return false;
411 }
207 412
208 if (asset == null) 413 if (asset == null || asset.Data == null || asset.Data.Length == 0)
209 { 414 {
210 lock (m_AssetHandlers) 415 lock (m_AssetHandlers)
211 { 416 {
212 AssetRetrievedEx handlerEx = new AssetRetrievedEx(delegate(AssetBase _asset) { handler(id, sender, _asset); }); 417 AssetRetrievedEx handlerEx = new AssetRetrievedEx(delegate(AssetBase _asset) { handler(id, sender, _asset); });
213 418
214 AssetRetrievedEx handlers; 419 List<AssetRetrievedEx> handlers;
215 if (m_AssetHandlers.TryGetValue(id, out handlers)) 420 if (m_AssetHandlers.TryGetValue(id, out handlers))
216 { 421 {
217 // Someone else is already loading this asset. It will notify our handler when done. 422 // Someone else is already loading this asset. It will notify our handler when done.
218 handlers += handlerEx; 423 handlers.Add(handlerEx);
219 return true; 424 return true;
220 } 425 }
221 426
222 // Load the asset ourselves 427 handlers = new List<AssetRetrievedEx>();
223 handlers += handlerEx; 428 handlers.Add(handlerEx);
224 m_AssetHandlers.Add(id, handlers);
225 }
226 429
227 bool success = false; 430 m_AssetHandlers.Add(id, handlers);
228 try
229 {
230 AsynchronousRestObjectRequester.MakeRequest<int, AssetBase>("GET", uri, 0,
231 delegate(AssetBase a)
232 {
233 if (a != null && m_Cache != null)
234 m_Cache.Cache(a);
235 431
236 AssetRetrievedEx handlers; 432 QueuedAssetRequest request = new QueuedAssetRequest();
237 lock (m_AssetHandlers) 433 request.id = id;
238 { 434 request.uri = uri;
239 handlers = m_AssetHandlers[id]; 435 m_requestQueue.Enqueue(request);
240 m_AssetHandlers.Remove(id);
241 }
242 handlers.Invoke(a);
243 }, m_maxAssetRequestConcurrency, m_Auth);
244
245 success = true;
246 }
247 finally
248 {
249 if (!success)
250 {
251 lock (m_AssetHandlers)
252 {
253 m_AssetHandlers.Remove(id);
254 }
255 }
256 } 436 }
257 } 437 }
258 else 438 else
@@ -277,52 +457,151 @@ namespace OpenSim.Services.Connectors
277 // This is most likely to happen because the server doesn't support this function, 457 // This is most likely to happen because the server doesn't support this function,
278 // so just silently return "doesn't exist" for all the assets. 458 // so just silently return "doesn't exist" for all the assets.
279 } 459 }
280 460
281 if (exist == null) 461 if (exist == null)
282 exist = new bool[ids.Length]; 462 exist = new bool[ids.Length];
283 463
284 return exist; 464 return exist;
285 } 465 }
286 466
467 string stringUUIDZero = UUID.Zero.ToString();
468
287 public string Store(AssetBase asset) 469 public string Store(AssetBase asset)
288 { 470 {
289 if (asset.Local) 471 // Have to assign the asset ID here. This isn't likely to
472 // trigger since current callers don't pass emtpy IDs
473 // We need the asset ID to route the request to the proper
474 // cluster member, so we can't have the server assign one.
475 if (asset.ID == string.Empty || asset.ID == stringUUIDZero)
290 { 476 {
291 if (m_Cache != null) 477 if (asset.FullID == UUID.Zero)
292 m_Cache.Cache(asset); 478 {
479 asset.FullID = UUID.Random();
480 }
481 m_log.WarnFormat("[Assets] Zero ID: {0}",asset.Name);
482 asset.ID = asset.FullID.ToString();
483 }
484
485 if (asset.FullID == UUID.Zero)
486 {
487 UUID uuid = UUID.Zero;
488 if (UUID.TryParse(asset.ID, out uuid))
489 {
490 asset.FullID = uuid;
491 }
492 if(asset.FullID == UUID.Zero)
493 {
494 m_log.WarnFormat("[Assets] Zero IDs: {0}",asset.Name);
495 asset.FullID = UUID.Random();
496 asset.ID = asset.FullID.ToString();
497 }
498 }
499
500 if (m_Cache != null)
501 m_Cache.Cache(asset);
293 502
503 if (asset.Temporary || asset.Local)
504 {
294 return asset.ID; 505 return asset.ID;
295 } 506 }
296 507
297 string uri = m_ServerURI + "/assets/"; 508 string uri = MapServer(asset.FullID.ToString()) + "/assets/";
298 509
299 string newID; 510 string newID = null;
300 try 511 try
301 { 512 {
302 newID = SynchronousRestObjectRequester.MakeRequest<AssetBase, string>("POST", uri, asset, m_Auth); 513 newID = SynchronousRestObjectRequester.
514 MakeRequest<AssetBase, string>("POST", uri, asset, 100000, m_Auth);
303 } 515 }
304 catch (Exception e) 516 catch
305 { 517 {
306 m_log.Warn(string.Format("[ASSET CONNECTOR]: Unable to send asset {0} to asset server. Reason: {1} ", asset.ID, e.Message), e); 518 newID = null;
307 return string.Empty;
308 } 519 }
309 520
310 // TEMPORARY: SRAS returns 'null' when it's asked to store existing assets 521 if (newID == null || newID == String.Empty || newID == stringUUIDZero)
311 if (newID == null)
312 { 522 {
313 m_log.DebugFormat("[ASSET CONNECTOR]: Storing of asset {0} returned null; assuming the asset already exists", asset.ID); 523 //The asset upload failed, try later
314 return asset.ID; 524 lock(m_sendRetries)
525 {
526 if (m_sendRetries[0] == null)
527 m_sendRetries[0] = new List<AssetBase>();
528 List<AssetBase> m_queue = m_sendRetries[0];
529 m_queue.Add(asset);
530 m_log.WarnFormat("[Assets] Upload failed: {0} type {1} will retry later",
531 asset.ID.ToString(), asset.Type.ToString());
532 m_retryTimer.Start();
533 }
315 } 534 }
535 else
536 {
537 if (newID != asset.ID)
538 {
539 // Placing this here, so that this work with old asset servers that don't send any reply back
540 // SynchronousRestObjectRequester returns somethins that is not an empty string
316 541
317 if (string.IsNullOrEmpty(newID)) 542 asset.ID = newID;
318 return string.Empty;
319 543
320 asset.ID = newID; 544 if (m_Cache != null)
545 m_Cache.Cache(asset);
546 }
547 }
548 return asset.ID;
549 }
321 550
322 if (m_Cache != null) 551 public void retryStore(AssetBase asset, int nextRetryLevel)
323 m_Cache.Cache(asset); 552 {
553/* this may be bad, so excluding
554 if (m_Cache != null && !m_Cache.Check(asset.ID))
555 {
556 m_log.WarnFormat("[Assets] Upload giveup asset bc no longer in local cache: {0}",
557 asset.ID.ToString();
558 return; // if no longer in cache, it was deleted or expired
559 }
560*/
561 string uri = MapServer(asset.FullID.ToString()) + "/assets/";
324 562
325 return newID; 563 string newID = null;
564 try
565 {
566 newID = SynchronousRestObjectRequester.
567 MakeRequest<AssetBase, string>("POST", uri, asset, 100000, m_Auth);
568 }
569 catch
570 {
571 newID = null;
572 }
573
574 if (newID == null || newID == String.Empty || newID == stringUUIDZero)
575 {
576 if(nextRetryLevel >= MAXSENDRETRIESLEN)
577 m_log.WarnFormat("[Assets] Upload giveup after several retries id: {0} type {1}",
578 asset.ID.ToString(), asset.Type.ToString());
579 else
580 {
581 lock(m_sendRetries)
582 {
583 if (m_sendRetries[nextRetryLevel] == null)
584 {
585 m_sendRetries[nextRetryLevel] = new List<AssetBase>();
586 }
587 List<AssetBase> m_queue = m_sendRetries[nextRetryLevel];
588 m_queue.Add(asset);
589 m_log.WarnFormat("[Assets] Upload failed: {0} type {1} will retry later",
590 asset.ID.ToString(), asset.Type.ToString());
591 }
592 }
593 }
594 else
595 {
596 m_log.InfoFormat("[Assets] Upload of {0} succeeded after {1} failed attempts", asset.ID.ToString(), nextRetryLevel.ToString());
597 if (newID != asset.ID)
598 {
599 asset.ID = newID;
600
601 if (m_Cache != null)
602 m_Cache.Cache(asset);
603 }
604 }
326 } 605 }
327 606
328 public bool UpdateContent(string id, byte[] data) 607 public bool UpdateContent(string id, byte[] data)
@@ -330,7 +609,7 @@ namespace OpenSim.Services.Connectors
330 AssetBase asset = null; 609 AssetBase asset = null;
331 610
332 if (m_Cache != null) 611 if (m_Cache != null)
333 asset = m_Cache.Get(id); 612 m_Cache.Get(id, out asset);
334 613
335 if (asset == null) 614 if (asset == null)
336 { 615 {
@@ -343,7 +622,7 @@ namespace OpenSim.Services.Connectors
343 } 622 }
344 asset.Data = data; 623 asset.Data = data;
345 624
346 string uri = m_ServerURI + "/assets/" + id; 625 string uri = MapServer(id) + "/assets/" + id;
347 626
348 if (SynchronousRestObjectRequester.MakeRequest<AssetBase, bool>("POST", uri, asset, m_Auth)) 627 if (SynchronousRestObjectRequester.MakeRequest<AssetBase, bool>("POST", uri, asset, m_Auth))
349 { 628 {
@@ -355,9 +634,10 @@ namespace OpenSim.Services.Connectors
355 return false; 634 return false;
356 } 635 }
357 636
637
358 public bool Delete(string id) 638 public bool Delete(string id)
359 { 639 {
360 string uri = m_ServerURI + "/assets/" + id; 640 string uri = MapServer(id) + "/assets/" + id;
361 641
362 if (SynchronousRestObjectRequester.MakeRequest<int, bool>("DELETE", uri, 0, m_Auth)) 642 if (SynchronousRestObjectRequester.MakeRequest<int, bool>("DELETE", uri, 0, m_Auth))
363 { 643 {