aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/OpenSim/Data/PGSQL/PGSQLXAssetData.cs
diff options
context:
space:
mode:
Diffstat (limited to 'OpenSim/Data/PGSQL/PGSQLXAssetData.cs')
-rw-r--r--OpenSim/Data/PGSQL/PGSQLXAssetData.cs587
1 files changed, 587 insertions, 0 deletions
diff --git a/OpenSim/Data/PGSQL/PGSQLXAssetData.cs b/OpenSim/Data/PGSQL/PGSQLXAssetData.cs
new file mode 100644
index 0000000..4f682f0
--- /dev/null
+++ b/OpenSim/Data/PGSQL/PGSQLXAssetData.cs
@@ -0,0 +1,587 @@
1/*
2 * Copyright (c) Contributors, http://opensimulator.org/
3 * See CONTRIBUTORS.TXT for a full list of copyright holders.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 * * Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * * Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 * * Neither the name of the OpenSimulator Project nor the
13 * names of its contributors may be used to endorse or promote products
14 * derived from this software without specific prior written permission.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
17 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
20 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27
28using System;
29using System.Collections.Generic;
30using System.Data;
31using System.IO;
32using System.IO.Compression;
33using System.Reflection;
34using System.Security.Cryptography;
35using System.Text;
36using log4net;
37using OpenMetaverse;
38using OpenSim.Framework;
39using OpenSim.Data;
40using Npgsql;
41
42namespace OpenSim.Data.PGSQL
43{
44 public class PGSQLXAssetData : IXAssetDataPlugin
45 {
46 private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
47
48 protected virtual Assembly Assembly
49 {
50 get { return GetType().Assembly; }
51 }
52
53 /// <summary>
54 /// Number of days that must pass before we update the access time on an asset when it has been fetched.
55 /// </summary>
56 private const int DaysBetweenAccessTimeUpdates = 30;
57
58 private bool m_enableCompression = false;
59 private PGSQLManager m_database;
60 private string m_connectionString;
61 private object m_dbLock = new object();
62
63 /// <summary>
64 /// We can reuse this for all hashing since all methods are single-threaded through m_dbBLock
65 /// </summary>
66 private HashAlgorithm hasher = new SHA256CryptoServiceProvider();
67
68 #region IPlugin Members
69
70 public string Version { get { return "1.0.0.0"; } }
71
72 /// <summary>
73 /// <para>Initialises Asset interface</para>
74 /// <para>
75 /// <list type="bullet">
76 /// <item>Loads and initialises the PGSQL storage plugin.</item>
77 /// <item>Warns and uses the obsolete pgsql_connection.ini if connect string is empty.</item>
78 /// <item>Check for migration</item>
79 /// </list>
80 /// </para>
81 /// </summary>
82 /// <param name="connect">connect string</param>
83 public void Initialise(string connect)
84 {
85 m_log.ErrorFormat("[PGSQL XASSETDATA]: ***********************************************************");
86 m_log.ErrorFormat("[PGSQL XASSETDATA]: ***********************************************************");
87 m_log.ErrorFormat("[PGSQL XASSETDATA]: ***********************************************************");
88 m_log.ErrorFormat("[PGSQL XASSETDATA]: THIS PLUGIN IS STRICTLY EXPERIMENTAL.");
89 m_log.ErrorFormat("[PGSQL XASSETDATA]: DO NOT USE FOR ANY DATA THAT YOU DO NOT MIND LOSING.");
90 m_log.ErrorFormat("[PGSQL XASSETDATA]: DATABASE TABLES CAN CHANGE AT ANY TIME, CAUSING EXISTING DATA TO BE LOST.");
91 m_log.ErrorFormat("[PGSQL XASSETDATA]: ***********************************************************");
92 m_log.ErrorFormat("[PGSQL XASSETDATA]: ***********************************************************");
93 m_log.ErrorFormat("[PGSQL XASSETDATA]: ***********************************************************");
94
95 m_connectionString = connect;
96 m_database = new PGSQLManager(m_connectionString);
97
98 using (NpgsqlConnection dbcon = new NpgsqlConnection(m_connectionString))
99 {
100 dbcon.Open();
101 Migration m = new Migration(dbcon, Assembly, "XAssetStore");
102 m.Update();
103 }
104 }
105
106 public void Initialise()
107 {
108 throw new NotImplementedException();
109 }
110
111 public void Dispose() { }
112
113 /// <summary>
114 /// The name of this DB provider
115 /// </summary>
116 public string Name
117 {
118 get { return "PGSQL XAsset storage engine"; }
119 }
120
121 #endregion
122
123 #region IAssetDataPlugin Members
124
125 /// <summary>
126 /// Fetch Asset <paramref name="assetID"/> from database
127 /// </summary>
128 /// <param name="assetID">Asset UUID to fetch</param>
129 /// <returns>Return the asset</returns>
130 /// <remarks>On failure : throw an exception and attempt to reconnect to database</remarks>
131 public AssetBase GetAsset(UUID assetID)
132 {
133// m_log.DebugFormat("[PGSQL XASSET DATA]: Looking for asset {0}", assetID);
134
135 AssetBase asset = null;
136 lock (m_dbLock)
137 {
138 using (NpgsqlConnection dbcon = new NpgsqlConnection(m_connectionString))
139 {
140 dbcon.Open();
141
142 using (NpgsqlCommand cmd = new NpgsqlCommand(
143 @"SELECT name, description, access_time, ""AssetType"", local, temporary, asset_flags, creatorid, data
144 FROM XAssetsMeta
145 JOIN XAssetsData ON XAssetsMeta.hash = XAssetsData.Hash WHERE id=:ID",
146 dbcon))
147 {
148 cmd.Parameters.Add(m_database.CreateParameter("ID", assetID));
149
150 try
151 {
152 using (NpgsqlDataReader dbReader = cmd.ExecuteReader(CommandBehavior.SingleRow))
153 {
154 if (dbReader.Read())
155 {
156 asset = new AssetBase(
157 assetID,
158 (string)dbReader["name"],
159 Convert.ToSByte(dbReader["AssetType"]),
160 dbReader["creatorid"].ToString());
161
162 asset.Data = (byte[])dbReader["data"];
163 asset.Description = (string)dbReader["description"];
164
165 string local = dbReader["local"].ToString();
166 if (local.Equals("1") || local.Equals("true", StringComparison.InvariantCultureIgnoreCase))
167 asset.Local = true;
168 else
169 asset.Local = false;
170
171 asset.Temporary = Convert.ToBoolean(dbReader["temporary"]);
172 asset.Flags = (AssetFlags)Convert.ToInt32(dbReader["asset_flags"]);
173
174 if (m_enableCompression)
175 {
176 using (GZipStream decompressionStream = new GZipStream(new MemoryStream(asset.Data), CompressionMode.Decompress))
177 {
178 MemoryStream outputStream = new MemoryStream();
179 WebUtil.CopyStream(decompressionStream, outputStream, int.MaxValue);
180 // int compressedLength = asset.Data.Length;
181 asset.Data = outputStream.ToArray();
182
183 // m_log.DebugFormat(
184 // "[XASSET DB]: Decompressed {0} {1} to {2} bytes from {3}",
185 // asset.ID, asset.Name, asset.Data.Length, compressedLength);
186 }
187 }
188
189 UpdateAccessTime(asset.Metadata, (int)dbReader["access_time"]);
190 }
191 }
192 }
193 catch (Exception e)
194 {
195 m_log.Error(string.Format("[PGSQL XASSET DATA]: Failure fetching asset {0}", assetID), e);
196 }
197 }
198 }
199 }
200
201 return asset;
202 }
203
204 /// <summary>
205 /// Create an asset in database, or update it if existing.
206 /// </summary>
207 /// <param name="asset">Asset UUID to create</param>
208 /// <remarks>On failure : Throw an exception and attempt to reconnect to database</remarks>
209 public void StoreAsset(AssetBase asset)
210 {
211// m_log.DebugFormat("[XASSETS DB]: Storing asset {0} {1}", asset.Name, asset.ID);
212
213 lock (m_dbLock)
214 {
215 using (NpgsqlConnection dbcon = new NpgsqlConnection(m_connectionString))
216 {
217 dbcon.Open();
218
219 using (NpgsqlTransaction transaction = dbcon.BeginTransaction())
220 {
221 string assetName = asset.Name;
222 if (asset.Name.Length > 64)
223 {
224 assetName = asset.Name.Substring(0, 64);
225 m_log.WarnFormat(
226 "[XASSET DB]: Name '{0}' for asset {1} truncated from {2} to {3} characters on add",
227 asset.Name, asset.ID, asset.Name.Length, assetName.Length);
228 }
229
230 string assetDescription = asset.Description;
231 if (asset.Description.Length > 64)
232 {
233 assetDescription = asset.Description.Substring(0, 64);
234 m_log.WarnFormat(
235 "[XASSET DB]: Description '{0}' for asset {1} truncated from {2} to {3} characters on add",
236 asset.Description, asset.ID, asset.Description.Length, assetDescription.Length);
237 }
238
239 if (m_enableCompression)
240 {
241 MemoryStream outputStream = new MemoryStream();
242
243 using (GZipStream compressionStream = new GZipStream(outputStream, CompressionMode.Compress, false))
244 {
245 // Console.WriteLine(WebUtil.CopyTo(new MemoryStream(asset.Data), compressionStream, int.MaxValue));
246 // We have to close the compression stream in order to make sure it writes everything out to the underlying memory output stream.
247 compressionStream.Close();
248 byte[] compressedData = outputStream.ToArray();
249 asset.Data = compressedData;
250 }
251 }
252
253 byte[] hash = hasher.ComputeHash(asset.Data);
254
255 UUID asset_id;
256 UUID.TryParse(asset.ID, out asset_id);
257
258// m_log.DebugFormat(
259// "[XASSET DB]: Compressed data size for {0} {1}, hash {2} is {3}",
260// asset.ID, asset.Name, hash, compressedData.Length);
261
262 try
263 {
264 using (NpgsqlCommand cmd =
265 new NpgsqlCommand(
266 @"insert INTO XAssetsMeta(id, hash, name, description, ""AssetType"", local, temporary, create_time, access_time, asset_flags, creatorid)
267 Select :ID, :Hash, :Name, :Description, :AssetType, :Local, :Temporary, :CreateTime, :AccessTime, :AssetFlags, :CreatorID
268 where not exists( Select id from XAssetsMeta where id = :ID);
269
270 update XAssetsMeta
271 set id = :ID, hash = :Hash, name = :Name, description = :Description,
272 ""AssetType"" = :AssetType, local = :Local, temporary = :Temporary, create_time = :CreateTime,
273 access_time = :AccessTime, asset_flags = :AssetFlags, creatorid = :CreatorID
274 where id = :ID;
275 ",
276 dbcon))
277 {
278
279 // create unix epoch time
280 int now = (int)Utils.DateTimeToUnixTime(DateTime.UtcNow);
281 cmd.Parameters.Add(m_database.CreateParameter("ID", asset_id));
282 cmd.Parameters.Add(m_database.CreateParameter("Hash", hash));
283 cmd.Parameters.Add(m_database.CreateParameter("Name", assetName));
284 cmd.Parameters.Add(m_database.CreateParameter("Description", assetDescription));
285 cmd.Parameters.Add(m_database.CreateParameter("AssetType", asset.Type));
286 cmd.Parameters.Add(m_database.CreateParameter("Local", asset.Local));
287 cmd.Parameters.Add(m_database.CreateParameter("Temporary", asset.Temporary));
288 cmd.Parameters.Add(m_database.CreateParameter("CreateTime", now));
289 cmd.Parameters.Add(m_database.CreateParameter("AccessTime", now));
290 cmd.Parameters.Add(m_database.CreateParameter("CreatorID", asset.Metadata.CreatorID));
291 cmd.Parameters.Add(m_database.CreateParameter("AssetFlags", (int)asset.Flags));
292
293 cmd.ExecuteNonQuery();
294 }
295 }
296 catch (Exception e)
297 {
298 m_log.ErrorFormat("[ASSET DB]: PGSQL failure creating asset metadata {0} with name \"{1}\". Error: {2}",
299 asset.FullID, asset.Name, e.Message);
300
301 transaction.Rollback();
302
303 return;
304 }
305
306 if (!ExistsData(dbcon, transaction, hash))
307 {
308 try
309 {
310 using (NpgsqlCommand cmd =
311 new NpgsqlCommand(
312 @"INSERT INTO XAssetsData(hash, data) VALUES(:Hash, :Data)",
313 dbcon))
314 {
315 cmd.Parameters.Add(m_database.CreateParameter("Hash", hash));
316 cmd.Parameters.Add(m_database.CreateParameter("Data", asset.Data));
317 cmd.ExecuteNonQuery();
318 }
319 }
320 catch (Exception e)
321 {
322 m_log.ErrorFormat("[XASSET DB]: PGSQL failure creating asset data {0} with name \"{1}\". Error: {2}",
323 asset.FullID, asset.Name, e.Message);
324
325 transaction.Rollback();
326
327 return;
328 }
329 }
330
331 transaction.Commit();
332 }
333 }
334 }
335 }
336
337 /// <summary>
338 /// Updates the access time of the asset if it was accessed above a given threshhold amount of time.
339 /// </summary>
340 /// <remarks>
341 /// This gives us some insight into assets which haven't ben accessed for a long period. This is only done
342 /// over the threshold time to avoid excessive database writes as assets are fetched.
343 /// </remarks>
344 /// <param name='asset'></param>
345 /// <param name='accessTime'></param>
346 private void UpdateAccessTime(AssetMetadata assetMetadata, int accessTime)
347 {
348 DateTime now = DateTime.UtcNow;
349
350 if ((now - Utils.UnixTimeToDateTime(accessTime)).TotalDays < DaysBetweenAccessTimeUpdates)
351 return;
352
353 lock (m_dbLock)
354 {
355 using (NpgsqlConnection dbcon = new NpgsqlConnection(m_connectionString))
356 {
357 dbcon.Open();
358 NpgsqlCommand cmd =
359 new NpgsqlCommand(@"update XAssetsMeta set access_time=:AccessTime where id=:ID", dbcon);
360
361 try
362 {
363 UUID asset_id;
364 UUID.TryParse(assetMetadata.ID, out asset_id);
365
366 using (cmd)
367 {
368 // create unix epoch time
369 cmd.Parameters.Add(m_database.CreateParameter("id", asset_id));
370 cmd.Parameters.Add(m_database.CreateParameter("access_time", (int)Utils.DateTimeToUnixTime(now)));
371 cmd.ExecuteNonQuery();
372 }
373 }
374 catch (Exception e)
375 {
376 m_log.ErrorFormat(
377 "[XASSET PGSQL DB]: Failure updating access_time for asset {0} with name {1} : {2}",
378 assetMetadata.ID, assetMetadata.Name, e.Message);
379 }
380 }
381 }
382 }
383
384 /// <summary>
385 /// We assume we already have the m_dbLock.
386 /// </summary>
387 /// TODO: need to actually use the transaction.
388 /// <param name="dbcon"></param>
389 /// <param name="transaction"></param>
390 /// <param name="hash"></param>
391 /// <returns></returns>
392 private bool ExistsData(NpgsqlConnection dbcon, NpgsqlTransaction transaction, byte[] hash)
393 {
394// m_log.DebugFormat("[ASSETS DB]: Checking for asset {0}", uuid);
395
396 bool exists = false;
397
398 using (NpgsqlCommand cmd = new NpgsqlCommand(@"SELECT hash FROM XAssetsData WHERE hash=:Hash", dbcon))
399 {
400 cmd.Parameters.Add(m_database.CreateParameter("Hash", hash));
401
402 try
403 {
404 using (NpgsqlDataReader dbReader = cmd.ExecuteReader(CommandBehavior.SingleRow))
405 {
406 if (dbReader.Read())
407 {
408// m_log.DebugFormat("[ASSETS DB]: Found asset {0}", uuid);
409 exists = true;
410 }
411 }
412 }
413 catch (Exception e)
414 {
415 m_log.ErrorFormat(
416 "[XASSETS DB]: PGSql failure in ExistsData fetching hash {0}. Exception {1}{2}",
417 hash, e.Message, e.StackTrace);
418 }
419 }
420
421 return exists;
422 }
423
424 /// <summary>
425 /// Check if the assets exist in the database.
426 /// </summary>
427 /// <param name="uuids">The assets' IDs</param>
428 /// <returns>For each asset: true if it exists, false otherwise</returns>
429 public bool[] AssetsExist(UUID[] uuids)
430 {
431 if (uuids.Length == 0)
432 return new bool[0];
433
434 HashSet<UUID> exist = new HashSet<UUID>();
435
436 string ids = "'" + string.Join("','", uuids) + "'";
437 string sql = string.Format(@"SELECT id FROM XAssetsMeta WHERE id IN ({0})", ids);
438
439 using (NpgsqlConnection conn = new NpgsqlConnection(m_connectionString))
440 {
441 conn.Open();
442 using (NpgsqlCommand cmd = new NpgsqlCommand(sql, conn))
443 {
444 using (NpgsqlDataReader reader = cmd.ExecuteReader())
445 {
446 while (reader.Read())
447 {
448 UUID id = DBGuid.FromDB(reader["id"]);
449 exist.Add(id);
450 }
451 }
452 }
453 }
454
455 bool[] results = new bool[uuids.Length];
456 for (int i = 0; i < uuids.Length; i++)
457 results[i] = exist.Contains(uuids[i]);
458 return results;
459 }
460
461 /// <summary>
462 /// Check if the asset exists in the database
463 /// </summary>
464 /// <param name="uuid">The asset UUID</param>
465 /// <returns>true if it exists, false otherwise.</returns>
466 public bool ExistsAsset(UUID uuid)
467 {
468// m_log.DebugFormat("[ASSETS DB]: Checking for asset {0}", uuid);
469
470 bool assetExists = false;
471
472 lock (m_dbLock)
473 {
474 using (NpgsqlConnection dbcon = new NpgsqlConnection(m_connectionString))
475 {
476 dbcon.Open();
477 using (NpgsqlCommand cmd = new NpgsqlCommand(@"SELECT id FROM XAssetsMeta WHERE id=:ID", dbcon))
478 {
479 cmd.Parameters.Add(m_database.CreateParameter("id", uuid));
480
481 try
482 {
483 using (NpgsqlDataReader dbReader = cmd.ExecuteReader(CommandBehavior.SingleRow))
484 {
485 if (dbReader.Read())
486 {
487// m_log.DebugFormat("[ASSETS DB]: Found asset {0}", uuid);
488 assetExists = true;
489 }
490 }
491 }
492 catch (Exception e)
493 {
494 m_log.Error(string.Format("[XASSETS DB]: PGSql failure fetching asset {0}", uuid), e);
495 }
496 }
497 }
498 }
499
500 return assetExists;
501 }
502
503
504 /// <summary>
505 /// Returns a list of AssetMetadata objects. The list is a subset of
506 /// the entire data set offset by <paramref name="start" /> containing
507 /// <paramref name="count" /> elements.
508 /// </summary>
509 /// <param name="start">The number of results to discard from the total data set.</param>
510 /// <param name="count">The number of rows the returned list should contain.</param>
511 /// <returns>A list of AssetMetadata objects.</returns>
512 public List<AssetMetadata> FetchAssetMetadataSet(int start, int count)
513 {
514 List<AssetMetadata> retList = new List<AssetMetadata>(count);
515
516 lock (m_dbLock)
517 {
518 using (NpgsqlConnection dbcon = new NpgsqlConnection(m_connectionString))
519 {
520 dbcon.Open();
521 NpgsqlCommand cmd = new NpgsqlCommand( @"SELECT name, description, access_time, ""AssetType"", temporary, id, asset_flags, creatorid
522 FROM XAssetsMeta
523 LIMIT :start, :count", dbcon);
524 cmd.Parameters.Add(m_database.CreateParameter("start", start));
525 cmd.Parameters.Add(m_database.CreateParameter("count", count));
526
527 try
528 {
529 using (NpgsqlDataReader dbReader = cmd.ExecuteReader())
530 {
531 while (dbReader.Read())
532 {
533 AssetMetadata metadata = new AssetMetadata();
534 metadata.Name = (string)dbReader["name"];
535 metadata.Description = (string)dbReader["description"];
536 metadata.Type = Convert.ToSByte(dbReader["AssetType"]);
537 metadata.Temporary = Convert.ToBoolean(dbReader["temporary"]);
538 metadata.Flags = (AssetFlags)Convert.ToInt32(dbReader["asset_flags"]);
539 metadata.FullID = DBGuid.FromDB(dbReader["id"]);
540 metadata.CreatorID = dbReader["creatorid"].ToString();
541
542 // We'll ignore this for now - it appears unused!
543// metadata.SHA1 = dbReader["hash"]);
544
545 UpdateAccessTime(metadata, (int)dbReader["access_time"]);
546
547 retList.Add(metadata);
548 }
549 }
550 }
551 catch (Exception e)
552 {
553 m_log.Error("[XASSETS DB]: PGSql failure fetching asset set" + Environment.NewLine + e.ToString());
554 }
555 }
556 }
557
558 return retList;
559 }
560
561 public bool Delete(string id)
562 {
563// m_log.DebugFormat("[XASSETS DB]: Deleting asset {0}", id);
564
565 lock (m_dbLock)
566 {
567 using (NpgsqlConnection dbcon = new NpgsqlConnection(m_connectionString))
568 {
569 dbcon.Open();
570
571 using (NpgsqlCommand cmd = new NpgsqlCommand(@"delete from XAssetsMeta where id=:ID", dbcon))
572 {
573 cmd.Parameters.Add(m_database.CreateParameter(id, id));
574 cmd.ExecuteNonQuery();
575 }
576
577 // TODO: How do we deal with data from deleted assets? Probably not easily reapable unless we
578 // keep a reference count (?)
579 }
580 }
581
582 return true;
583 }
584
585 #endregion
586 }
587}