PageRenderTime 53ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/Raven.Database/Server/Controllers/StudioTasksController.cs

https://github.com/nwendel/ravendb
C# | 523 lines | 447 code | 67 blank | 9 comment | 57 complexity | de49f961c9b23f76522ed6a77a439cdd MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, BSD-3-Clause, CC-BY-SA-3.0
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.Specialized;
  4. using System.Globalization;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Net;
  8. using System.Net.Http;
  9. using System.Net.Http.Headers;
  10. using System.Security.Cryptography;
  11. using System.Text;
  12. using System.Threading;
  13. using System.Threading.Tasks;
  14. using System.Web;
  15. using System.Web.Http;
  16. using Microsoft.VisualBasic.FileIO;
  17. using Newtonsoft.Json;
  18. using Raven.Abstractions;
  19. using Raven.Abstractions.Commands;
  20. using Raven.Abstractions.Data;
  21. using Raven.Abstractions.Extensions;
  22. using Raven.Abstractions.Json;
  23. using Raven.Abstractions.Smuggler;
  24. using Raven.Abstractions.Util;
  25. using Raven.Client.Util;
  26. using Raven.Database.Actions;
  27. using Raven.Database.Bundles.SqlReplication;
  28. using Raven.Database.Smuggler;
  29. using Raven.Json.Linq;
  30. namespace Raven.Database.Server.Controllers
  31. {
  32. public class StudioTasksController : RavenDbApiController
  33. {
  34. const int csvImportBatchSize = 512;
  35. [HttpPost]
  36. [Route("studio-tasks/import")]
  37. [Route("databases/{databaseName}/studio-tasks/import")]
  38. public async Task<HttpResponseMessage> ImportDatabase(int batchSize, bool includeExpiredDocuments, ItemType operateOnTypes, string filtersPipeDelimited, string transformScript)
  39. {
  40. if (!Request.Content.IsMimeMultipartContent())
  41. {
  42. throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
  43. }
  44. string tempPath = Path.GetTempPath();
  45. var fullTempPath = tempPath + Constants.TempUploadsDirectoryName;
  46. if (File.Exists(fullTempPath))
  47. File.Delete(fullTempPath);
  48. if (Directory.Exists(fullTempPath) == false)
  49. Directory.CreateDirectory(fullTempPath);
  50. var streamProvider = new MultipartFileStreamProvider(fullTempPath);
  51. await Request.Content.ReadAsMultipartAsync(streamProvider);
  52. var uploadedFilePath = streamProvider.FileData[0].LocalFileName;
  53. string fileName = null;
  54. var fileContent = streamProvider.Contents.SingleOrDefault();
  55. if (fileContent != null)
  56. {
  57. fileName = fileContent.Headers.ContentDisposition.FileName.Replace("\"", string.Empty);
  58. }
  59. var status = new ImportOperationStatus();
  60. var cts = new CancellationTokenSource();
  61. var task = Task.Run(async () =>
  62. {
  63. try
  64. {
  65. using (var fileStream = File.Open(uploadedFilePath, FileMode.Open, FileAccess.Read))
  66. {
  67. var dataDumper = new DataDumper(Database);
  68. dataDumper.Progress += s => status.LastProgress = s;
  69. var smugglerOptions = dataDumper.SmugglerOptions;
  70. smugglerOptions.BatchSize = batchSize;
  71. smugglerOptions.ShouldExcludeExpired = !includeExpiredDocuments;
  72. smugglerOptions.OperateOnTypes = operateOnTypes;
  73. smugglerOptions.TransformScript = transformScript;
  74. smugglerOptions.CancelToken = cts;
  75. // Filters are passed in without the aid of the model binder. Instead, we pass in a list of FilterSettings using a string like this: pathHere;;;valueHere;;;true|||againPathHere;;;anotherValue;;;false
  76. // Why? Because I don't see a way to pass a list of a values to a WebAPI method that accepts a file upload, outside of passing in a simple string value and parsing it ourselves.
  77. if (filtersPipeDelimited != null)
  78. {
  79. smugglerOptions.Filters.AddRange(filtersPipeDelimited
  80. .Split(new string[] { "|||" }, StringSplitOptions.RemoveEmptyEntries)
  81. .Select(f => f.Split(new string[] { ";;;" }, StringSplitOptions.RemoveEmptyEntries))
  82. .Select(o => new FilterSetting { Path = o[0], Values = new List<string> { o[1] }, ShouldMatch = bool.Parse(o[2]) }));
  83. }
  84. await dataDumper.ImportData(new SmugglerImportOptions { FromStream = fileStream });
  85. }
  86. }
  87. catch (Exception e)
  88. {
  89. status.ExceptionDetails = e.ToString();
  90. if (cts.Token.IsCancellationRequested)
  91. {
  92. status.ExceptionDetails = "Task was cancelled";
  93. cts.Token.ThrowIfCancellationRequested(); //needed for displaying the task status as canceled and not faulted
  94. }
  95. throw;
  96. }
  97. finally
  98. {
  99. status.Completed = true;
  100. File.Delete(uploadedFilePath);
  101. }
  102. }, cts.Token);
  103. long id;
  104. Database.Tasks.AddTask(task, status, new TaskActions.PendingTaskDescription
  105. {
  106. StartTime = SystemTime.UtcNow,
  107. TaskType = TaskActions.PendingTaskType.ImportDatabase,
  108. Payload = fileName,
  109. }, out id, cts);
  110. return GetMessageWithObject(new
  111. {
  112. OperationId = id
  113. });
  114. }
  115. public class ExportData
  116. {
  117. public string SmugglerOptions { get; set; }
  118. }
  119. [HttpPost]
  120. [Route("studio-tasks/exportDatabase")]
  121. [Route("databases/{databaseName}/studio-tasks/exportDatabase")]
  122. public Task<HttpResponseMessage> ExportDatabase(ExportData smugglerOptionsJson)
  123. {
  124. var requestString = smugglerOptionsJson.SmugglerOptions;
  125. SmugglerOptions smugglerOptions;
  126. using (var jsonReader = new RavenJsonTextReader(new StringReader(requestString)))
  127. {
  128. var serializer = JsonExtensions.CreateDefaultJsonSerializer();
  129. smugglerOptions = (SmugglerOptions)serializer.Deserialize(jsonReader, typeof(SmugglerOptions));
  130. }
  131. var result = GetEmptyMessage();
  132. // create PushStreamContent object that will be called when the output stream will be ready.
  133. result.Content = new PushStreamContent(async (outputStream, content, arg3) =>
  134. {
  135. try
  136. {
  137. var dataDumper = new DataDumper(Database, smugglerOptions);
  138. await dataDumper.ExportData(
  139. new SmugglerExportOptions
  140. {
  141. ToStream = outputStream
  142. }).ConfigureAwait(false);
  143. }
  144. finally
  145. {
  146. outputStream.Close();
  147. }
  148. });
  149. result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
  150. {
  151. FileName = string.Format("Dump of {0}, {1}.ravendump", this.DatabaseName, DateTime.Now.ToString("yyyy-MM-dd HH-mm", CultureInfo.InvariantCulture))
  152. };
  153. return new CompletedTask<HttpResponseMessage>(result);
  154. }
  155. [HttpPost]
  156. [Route("studio-tasks/createSampleData")]
  157. [Route("databases/{databaseName}/studio-tasks/createSampleData")]
  158. public async Task<HttpResponseMessage> CreateSampleData()
  159. {
  160. var results = Database.Queries.Query(Constants.DocumentsByEntityNameIndex, new IndexQuery(), CancellationToken.None);
  161. if (results.Results.Count > 0)
  162. {
  163. return GetMessageWithString("You cannot create sample data in a database that already contains documents", HttpStatusCode.BadRequest);
  164. }
  165. using (var sampleData = typeof(StudioTasksController).Assembly.GetManifestResourceStream("Raven.Database.Server.Assets.EmbeddedData.Northwind.dump"))
  166. {
  167. var dataDumper = new DataDumper(Database) {SmugglerOptions = {OperateOnTypes = ItemType.Documents | ItemType.Indexes | ItemType.Transformers, ShouldExcludeExpired = false}};
  168. await dataDumper.ImportData(new SmugglerImportOptions {FromStream = sampleData});
  169. }
  170. return GetEmptyMessage();
  171. }
  172. [HttpGet]
  173. [Route("studio-tasks/simulate-sql-replication")]
  174. [Route("databases/{databaseName}/studio-tasks/simulate-sql-replication")]
  175. public Task<HttpResponseMessage> SimulateSqlReplication(string documentId, bool performRolledBackTransaction)
  176. {
  177. var task = Database.StartupTasks.OfType<SqlReplicationTask>().FirstOrDefault();
  178. if (task == null)
  179. return GetMessageWithObjectAsTask(new
  180. {
  181. Error = "SQL Replication bundle is not installed"
  182. }, HttpStatusCode.NotFound);
  183. try
  184. {
  185. Alert alert = null;
  186. var sqlReplication =
  187. JsonConvert.DeserializeObject<SqlReplicationConfig>(GetQueryStringValue("sqlReplication"));
  188. // string strDocumentId, SqlReplicationConfig sqlReplication, bool performRolledbackTransaction, out Alert alert, out Dictionary<string,object> parameters
  189. var results = task.SimulateSqlReplicationSQLQueries(documentId, sqlReplication, performRolledBackTransaction, out alert);
  190. return GetMessageWithObjectAsTask(new {
  191. Results = results,
  192. LastAlert = alert
  193. });
  194. }
  195. catch (Exception ex)
  196. {
  197. return GetMessageWithObjectAsTask(new
  198. {
  199. Error = "Executeion failed",
  200. Exception = ex
  201. }, HttpStatusCode.BadRequest);
  202. }
  203. }
  204. [HttpGet]
  205. [Route("studio-tasks/test-sql-replication-connection")]
  206. [Route("databases/{databaseName}/studio-tasks/test-sql-replication-connection")]
  207. public Task<HttpResponseMessage> TestSqlReplicationConnection(string factoryName, string connectionString)
  208. {
  209. try
  210. {
  211. RelationalDatabaseWriter.TestConnection(factoryName, connectionString);
  212. return GetEmptyMessageAsTask(HttpStatusCode.NoContent);
  213. }
  214. catch (Exception ex)
  215. {
  216. return GetMessageWithObjectAsTask(new
  217. {
  218. Error = "Connection failed",
  219. Exception = ex
  220. }, HttpStatusCode.BadRequest);
  221. }
  222. }
  223. [HttpGet]
  224. [Route("studio-tasks/createSampleDataClass")]
  225. [Route("databases/{databaseName}/studio-tasks/createSampleDataClass")]
  226. public Task<HttpResponseMessage> CreateSampleDataClass()
  227. {
  228. using (var sampleData = typeof(StudioTasksController).Assembly.GetManifestResourceStream("Raven.Database.Server.Assets.EmbeddedData.NorthwindHelpData.cs"))
  229. {
  230. if (sampleData == null)
  231. return GetEmptyMessageAsTask();
  232. sampleData.Position = 0;
  233. using (var reader = new StreamReader(sampleData, Encoding.UTF8))
  234. {
  235. var data = reader.ReadToEnd();
  236. return GetMessageWithObjectAsTask(data);
  237. }
  238. }
  239. }
  240. [HttpGet]
  241. [Route("studio-tasks/new-encryption-key")]
  242. public HttpResponseMessage GetNewEncryption(string path = null)
  243. {
  244. RandomNumberGenerator randomNumberGenerator = new RNGCryptoServiceProvider();
  245. var byteStruct = new byte[Constants.DefaultGeneratedEncryptionKeyLength];
  246. randomNumberGenerator.GetBytes(byteStruct);
  247. var result = Convert.ToBase64String(byteStruct);
  248. HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK, result);
  249. return response;
  250. }
  251. [HttpGet]
  252. [Route("studio-tasks/get-sql-replication-stats")]
  253. [Route("databases/{databaseName}/studio-tasks/get-sql-replication-stats")]
  254. public HttpResponseMessage GetSQLReplicationStats(string sqlReplicationName)
  255. {
  256. var task = Database.StartupTasks.OfType<SqlReplicationTask>().FirstOrDefault();
  257. if (task == null)
  258. return GetMessageWithObject(new
  259. {
  260. Error = "SQL Replication bundle is not installed"
  261. }, HttpStatusCode.NotFound);
  262. var matchingStats = task.Statistics.FirstOrDefault(x => x.Key == sqlReplicationName);
  263. if (matchingStats.Key != null)
  264. {
  265. return GetMessageWithObject(task.Statistics.FirstOrDefault(x => x.Key == sqlReplicationName));
  266. }
  267. return GetEmptyMessage(HttpStatusCode.NotFound);
  268. }
  269. [HttpPost]
  270. [Route("studio-tasks/reset-sql-replication")]
  271. [Route("databases/{databaseName}/studio-tasks/reset-sql-replication")]
  272. public Task<HttpResponseMessage> ResetSqlReplication(string sqlReplicationName)
  273. {
  274. var task = Database.StartupTasks.OfType<SqlReplicationTask>().FirstOrDefault();
  275. if (task == null)
  276. return GetMessageWithObjectAsTask(new
  277. {
  278. Error = "SQL Replication bundle is not installed"
  279. }, HttpStatusCode.NotFound);
  280. SqlReplicationStatistics stats;
  281. task.Statistics.TryRemove(sqlReplicationName, out stats);
  282. var jsonDocument = Database.Documents.Get(SqlReplicationTask.RavenSqlreplicationStatus, null);
  283. if (jsonDocument != null)
  284. {
  285. var replicationStatus = jsonDocument.DataAsJson.JsonDeserialization<SqlReplicationStatus>();
  286. replicationStatus.LastReplicatedEtags.RemoveAll(x => x.Name == sqlReplicationName);
  287. Database.Documents.Put(SqlReplicationTask.RavenSqlreplicationStatus, null, RavenJObject.FromObject(replicationStatus), new RavenJObject(), null);
  288. }
  289. return GetEmptyMessageAsTask(HttpStatusCode.NoContent);
  290. }
  291. [HttpPost]
  292. [Route("studio-tasks/is-base-64-key")]
  293. public async Task<HttpResponseMessage> IsBase64Key(string path = null)
  294. {
  295. string message = null;
  296. try
  297. {
  298. //Request is of type HttpRequestMessage
  299. string keyObjectString = await Request.Content.ReadAsStringAsync();
  300. NameValueCollection nvc = HttpUtility.ParseQueryString(keyObjectString);
  301. var key = nvc["key"];
  302. //Convert base64-encoded hash value into a byte array.
  303. //ReSharper disable once ReturnValueOfPureMethodIsNotUsed
  304. Convert.FromBase64String(key);
  305. }
  306. catch (Exception)
  307. {
  308. message = "The key must be in Base64 encoding format!";
  309. }
  310. HttpResponseMessage response = Request.CreateResponse((message == null) ? HttpStatusCode.OK : HttpStatusCode.BadRequest, message);
  311. return response;
  312. }
  313. private Task FlushBatch(IEnumerable<RavenJObject> batch)
  314. {
  315. var commands = (from doc in batch
  316. let metadata = doc.Value<RavenJObject>("@metadata")
  317. let removal = doc.Remove("@metadata")
  318. select new PutCommandData
  319. {
  320. Metadata = metadata,
  321. Document = doc,
  322. Key = metadata.Value<string>("@id"),
  323. }).ToArray();
  324. Database.Batch(commands, CancellationToken.None);
  325. return new CompletedTask();
  326. }
  327. [HttpGet]
  328. [Route("studio-tasks/resolveMerge")]
  329. [Route("databases/{databaseName}/studio-tasks/resolveMerge")]
  330. public Task<HttpResponseMessage> ResolveMerge(string documentId)
  331. {
  332. int nextPage = 0;
  333. var docs = Database.Documents.GetDocumentsWithIdStartingWith(documentId + "/conflicts", null, null, 0, 1024, CancellationToken.None, ref nextPage);
  334. var conflictsResolver = new ConflictsResolver(docs.Values<RavenJObject>());
  335. return GetMessageWithObjectAsTask(conflictsResolver.Resolve());
  336. }
  337. [HttpPost]
  338. [Route("studio-tasks/loadCsvFile")]
  339. [Route("databases/{databaseName}/studio-tasks/loadCsvFile")]
  340. public async Task<HttpResponseMessage> LoadCsvFile()
  341. {
  342. if (!Request.Content.IsMimeMultipartContent())
  343. throw new Exception(); // divided by zero
  344. var provider = new MultipartMemoryStreamProvider();
  345. await Request.Content.ReadAsMultipartAsync(provider);
  346. foreach (var file in provider.Contents)
  347. {
  348. var filename = file.Headers.ContentDisposition.FileName.Trim('\"');
  349. var stream = await file.ReadAsStreamAsync();
  350. using (var csvReader = new TextFieldParser(stream))
  351. {
  352. csvReader.SetDelimiters(",");
  353. var headers = csvReader.ReadFields();
  354. var entity =
  355. Inflector.Pluralize(CSharpClassName.ConvertToValidClassName(Path.GetFileNameWithoutExtension(filename)));
  356. if (entity.Length > 0 && char.IsLower(entity[0]))
  357. entity = char.ToUpper(entity[0]) + entity.Substring(1);
  358. var totalCount = 0;
  359. var batch = new List<RavenJObject>();
  360. var columns = headers.Where(x => x.StartsWith("@") == false).ToArray();
  361. batch.Clear();
  362. while (csvReader.EndOfData == false)
  363. {
  364. var record = csvReader.ReadFields();
  365. var document = new RavenJObject();
  366. string id = null;
  367. RavenJObject metadata = null;
  368. for (int index = 0; index < columns.Length; index++)
  369. {
  370. var column = columns[index];
  371. if (string.IsNullOrEmpty(column))
  372. continue;
  373. if (string.Equals("id", column, StringComparison.OrdinalIgnoreCase))
  374. {
  375. id = record[index];
  376. }
  377. else if (string.Equals(Constants.RavenEntityName, column, StringComparison.OrdinalIgnoreCase))
  378. {
  379. metadata = metadata ?? new RavenJObject();
  380. metadata[Constants.RavenEntityName] = record[index];
  381. id = id ?? record[index] + "/";
  382. }
  383. else if (string.Equals(Constants.RavenClrType, column, StringComparison.OrdinalIgnoreCase))
  384. {
  385. metadata = metadata ?? new RavenJObject();
  386. metadata[Constants.RavenClrType] = record[index];
  387. id = id ?? record[index] + "/";
  388. }
  389. else
  390. {
  391. document[column] = SetValueInDocument(record[index]);
  392. }
  393. }
  394. metadata = metadata ?? new RavenJObject { { "Raven-Entity-Name", entity } };
  395. document.Add("@metadata", metadata);
  396. metadata.Add("@id", id ?? Guid.NewGuid().ToString());
  397. batch.Add(document);
  398. totalCount++;
  399. if (batch.Count >= csvImportBatchSize)
  400. {
  401. await FlushBatch(batch);
  402. batch.Clear();
  403. }
  404. }
  405. if (batch.Count > 0)
  406. {
  407. await FlushBatch(batch);
  408. }
  409. }
  410. }
  411. return GetEmptyMessage();
  412. }
  413. private static RavenJToken SetValueInDocument(string value)
  414. {
  415. if (string.IsNullOrEmpty(value))
  416. return value;
  417. var ch = value[0];
  418. if (ch == '[' || ch == '{')
  419. {
  420. try
  421. {
  422. return RavenJToken.Parse(value);
  423. }
  424. catch (Exception)
  425. {
  426. // ignoring failure to parse, will proceed to insert as a string value
  427. }
  428. }
  429. else if (char.IsDigit(ch) || ch == '-' || ch == '.')
  430. {
  431. // maybe it is a number?
  432. long longResult;
  433. if (long.TryParse(value, out longResult))
  434. {
  435. return longResult;
  436. }
  437. decimal decimalResult;
  438. if (decimal.TryParse(value, out decimalResult))
  439. {
  440. return decimalResult;
  441. }
  442. }
  443. else if (ch == '"' && value.Length > 1 && value[value.Length - 1] == '"')
  444. {
  445. return value.Substring(1, value.Length - 2);
  446. }
  447. return value;
  448. }
  449. private class ImportOperationStatus
  450. {
  451. public bool Completed { get; set; }
  452. public string LastProgress { get; set; }
  453. public string ExceptionDetails { get; set; }
  454. }
  455. }
  456. }