Skip to content

Commit a824475

Browse files
committed
Web-API: Bridge to import framework: uploading import files to import profile directory
1 parent e96f704 commit a824475

21 files changed

Lines changed: 414 additions & 145 deletions

File tree

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
* #654 Place user agreement for downloadable files in checkout process
4848
* #398 EU law: add 'revocation' form and revocation waiver for ESD
4949
* #738 Implement download of pictures via URLs in product import
50+
* Web-API: Bridge to import framework: uploading import files to import profile directory
5051

5152
### Improvements
5253
* (Perf) Implemented static caches for URL aliases and localized properties. Increases app startup and request speed by up to 30%.

src/Libraries/SmartStore.Data/Migrations/201601262000441_ImportFramework1.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,10 @@ public void MigrateLocaleResources(LocaleResourcesBuilder builder)
403403
"Zeitlimit für Bilder-Download (Minuten)",
404404
"Specifies the timeout for the image download in minutes.",
405405
"Legt das Zeitlimit für den Bilder-Download in Minuten fest.");
406+
407+
builder.AddOrUpdate("Admin.System.Maintenance.SqlQuery.Succeeded",
408+
"The SQL command was executed successfully.",
409+
"Die SQL Anweisung wurde erfolgreich ausgeführt.");
406410
}
407411
}
408412
}

src/Libraries/SmartStore.Services/DataExchange/Import/IImportProfileService.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ public interface IImportProfileService
4242
/// <returns>Import profile</returns>
4343
ImportProfile GetImportProfileById(int id);
4444

45+
/// <summary>
46+
/// Gets an import profile by name
47+
/// </summary>
48+
/// <param name="name">Name of the import profile</param>
49+
/// <returns>Import profile</returns>
50+
ImportProfile GetImportProfileByName(string name);
51+
4552
/// <summary>
4653
/// Get all importable entity properties and their localized values
4754
/// </summary>

src/Libraries/SmartStore.Services/DataExchange/Import/ImportProfileService.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,18 @@ public virtual ImportProfile GetImportProfileById(int id)
235235
return profile;
236236
}
237237

238+
public virtual ImportProfile GetImportProfileByName(string name)
239+
{
240+
if (name.IsEmpty())
241+
return null;
242+
243+
var profile = _importProfileRepository.Table
244+
.Expand(x => x.ScheduleTask)
245+
.FirstOrDefault(x => x.Name == name);
246+
247+
return profile;
248+
}
249+
238250
public virtual Dictionary<string, string> GetImportableEntityProperties(ImportEntityType entityType)
239251
{
240252
if (_entityProperties == null)

src/Plugins/SmartStore.WebApi/Controllers/Api/UploadsController.cs

Lines changed: 157 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,81 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4+
using System.IO.Compression;
45
using System.Linq;
56
using System.Net.Http;
7+
using System.Net.Http.Headers;
8+
using System.Threading;
69
using System.Threading.Tasks;
710
using System.Web.Http;
811
using SmartStore.Core;
12+
using SmartStore.Core.Domain;
913
using SmartStore.Core.Domain.Catalog;
14+
using SmartStore.Core.Domain.DataExchange;
1015
using SmartStore.Core.Domain.Media;
16+
using SmartStore.Core.IO;
1117
using SmartStore.Services.Catalog;
18+
using SmartStore.Services.DataExchange.Import;
1219
using SmartStore.Services.Media;
1320
using SmartStore.Utilities;
21+
using SmartStore.Utilities.Threading;
1422
using SmartStore.Web.Framework.WebApi;
1523
using SmartStore.Web.Framework.WebApi.OData;
1624
using SmartStore.Web.Framework.WebApi.Security;
1725
using SmartStore.WebApi.Models.Api;
1826

1927
namespace SmartStore.WebApi.Controllers.Api
2028
{
29+
/// <see cref="http://www.asp.net/web-api/overview/advanced/sending-html-form-data,-part-2"/>
2130
public class UploadsController : ApiController
2231
{
32+
private static readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
33+
2334
private readonly Lazy<IProductService> _productService;
2435
private readonly Lazy<IPictureService> _pictureService;
36+
private readonly Lazy<IImportProfileService> _importProfileService;
2537
private readonly Lazy<IStoreContext> _storeContext;
2638
private readonly Lazy<MediaSettings> _mediaSettings;
2739

2840
public UploadsController(
2941
Lazy<IProductService> productService,
3042
Lazy<IPictureService> pictureService,
43+
Lazy<IImportProfileService> importProfileService,
3144
Lazy<IStoreContext> storeContext,
3245
Lazy<MediaSettings> mediaSettings)
3346
{
3447
_productService = productService;
3548
_pictureService = pictureService;
49+
_importProfileService = importProfileService;
3650
_storeContext = storeContext;
3751
_mediaSettings = mediaSettings;
3852
}
3953

40-
/// <see cref="http://www.asp.net/web-api/overview/advanced/sending-html-form-data,-part-2"/>
54+
#region Utilities
55+
56+
private StringContent CloneHeaderContent(string path, MultipartFileData origin)
57+
{
58+
var content = new StringContent(path);
59+
60+
ContentDispositionHeaderValue disposition;
61+
ContentDispositionHeaderValue.TryParse(origin.Headers.ContentDisposition.ToString(), out disposition);
62+
63+
content.Headers.ContentDisposition = disposition;
64+
65+
content.Headers.ContentDisposition.Name = origin.Headers.ContentDisposition.Name.ToUnquoted();
66+
content.Headers.ContentDisposition.FileName = Path.GetFileName(path);
67+
68+
content.Headers.ContentType.MediaType = MimeTypes.MapNameToMimeType(path);
69+
70+
return content;
71+
}
72+
73+
#endregion
74+
75+
[HttpPost]
4176
[WebApiAuthenticate(Permission = "ManageCatalog")]
4277
[WebApiQueryable(PagingOptional = true)]
43-
public async Task<IQueryable<UploadImage>> PostProductImages()
78+
public async Task<IQueryable<UploadImage>> ProductImages()
4479
{
4580
if (!Request.Content.IsMimeMultipartContent())
4681
{
@@ -56,10 +91,10 @@ public async Task<IQueryable<UploadImage>> PostProductImages()
5691
{
5792
await Request.Content.ReadAsMultipartAsync(provider);
5893
}
59-
catch (Exception exc)
94+
catch (Exception exception)
6095
{
6196
provider.DeleteLocalFiles();
62-
throw this.ExceptionInternalServerError(exc);
97+
throw this.ExceptionInternalServerError(exception);
6398
}
6499

65100
// find product entity
@@ -82,7 +117,7 @@ public async Task<IQueryable<UploadImage>> PostProductImages()
82117
if (entity == null)
83118
{
84119
provider.DeleteLocalFiles();
85-
throw this.ExceptionNotFound(WebApiGlobal.Error.EntityNotFound.FormatWith(identifier.NaIfEmpty()));
120+
throw this.ExceptionNotFound(WebApiGlobal.Error.EntityNotFound.FormatInvariant(identifier.NaIfEmpty()));
86121
}
87122

88123
// process images
@@ -97,15 +132,7 @@ public async Task<IQueryable<UploadImage>> PostProductImages()
97132

98133
foreach (var file in provider.FileData)
99134
{
100-
var image = new UploadImage
101-
{
102-
FileName = file.Headers.ContentDisposition.FileName.ToUnquoted(),
103-
Name = file.Headers.ContentDisposition.Name.ToUnquoted(),
104-
ContentDisposition = file.Headers.ContentDisposition.Parameters
105-
};
106-
107-
if (file.Headers.ContentType != null)
108-
image.MediaType = file.Headers.ContentType.MediaType.ToUnquoted();
135+
var image = new UploadImage(file.Headers);
109136

110137
if (image.FileName.IsEmpty())
111138
image.FileName = entity.Name;
@@ -115,7 +142,6 @@ public async Task<IQueryable<UploadImage>> PostProductImages()
115142
if (pictureBinary != null && pictureBinary.Length > 0)
116143
{
117144
pictureBinary = _pictureService.Value.ValidatePicture(pictureBinary);
118-
119145
pictureBinary = _pictureService.Value.FindEqualPicture(pictureBinary, pictures, out equalPictureId);
120146

121147
if (pictureBinary != null)
@@ -157,5 +183,121 @@ public async Task<IQueryable<UploadImage>> PostProductImages()
157183
provider.DeleteLocalFiles();
158184
return result.AsQueryable();
159185
}
186+
187+
[HttpPost]
188+
[WebApiAuthenticate(Permission = "ManageImports")]
189+
[WebApiQueryable(PagingOptional = true)]
190+
public async Task<IQueryable<UploadImportFile>> ImportFiles()
191+
{
192+
if (!Request.Content.IsMimeMultipartContent())
193+
{
194+
throw this.ExceptionUnsupportedMediaType();
195+
}
196+
197+
ImportProfile profile = null;
198+
string identifier = null;
199+
var tempDir = FileSystemHelper.TempDir(Guid.NewGuid().ToString());
200+
var provider = new MultipartFormDataStreamProvider(tempDir);
201+
202+
try
203+
{
204+
await Request.Content.ReadAsMultipartAsync(provider);
205+
}
206+
catch (Exception exception)
207+
{
208+
FileSystemHelper.ClearDirectory(tempDir, true);
209+
throw this.ExceptionInternalServerError(exception);
210+
}
211+
212+
// find import profile
213+
if (provider.FormData.AllKeys.Contains("Id"))
214+
{
215+
identifier = provider.FormData.GetValues("Id").FirstOrDefault();
216+
profile = _importProfileService.Value.GetImportProfileById(identifier.ToInt());
217+
}
218+
else if (provider.FormData.AllKeys.Contains("Name"))
219+
{
220+
identifier = provider.FormData.GetValues("Name").FirstOrDefault();
221+
profile = _importProfileService.Value.GetImportProfileByName(identifier);
222+
}
223+
224+
if (profile == null)
225+
{
226+
FileSystemHelper.ClearDirectory(tempDir, true);
227+
throw this.ExceptionNotFound(WebApiGlobal.Error.EntityNotFound.FormatInvariant(identifier.NaIfEmpty()));
228+
}
229+
230+
var deleteExisting = false;
231+
var result = new List<UploadImportFile>();
232+
var unzippedFiles = new List<MultipartFileData>();
233+
var importFolder = profile.GetImportFolder(true, true);
234+
var csvTypes = new string[] { ".csv", ".txt", ".tab" };
235+
236+
if (provider.FormData.AllKeys.Contains("deleteExisting"))
237+
{
238+
var strDeleteExisting = provider.FormData.GetValues("deleteExisting").FirstOrDefault();
239+
deleteExisting = (strDeleteExisting.HasValue() && strDeleteExisting.ToBool());
240+
}
241+
242+
// unzip files
243+
foreach (var file in provider.FileData)
244+
{
245+
var import = new UploadImportFile(file.Headers);
246+
247+
if (import.FileExtension.IsCaseInsensitiveEqual(".zip"))
248+
{
249+
var subDir = Path.Combine(tempDir, Guid.NewGuid().ToString());
250+
ZipFile.ExtractToDirectory(file.LocalFileName, subDir);
251+
FileSystemHelper.Delete(file.LocalFileName);
252+
253+
foreach (var unzippedFile in Directory.GetFiles(subDir, "*.*"))
254+
{
255+
var content = CloneHeaderContent(unzippedFile, file);
256+
unzippedFiles.Add(new MultipartFileData(content.Headers, unzippedFile));
257+
}
258+
}
259+
else
260+
{
261+
unzippedFiles.Add(new MultipartFileData(file.Headers, file.LocalFileName));
262+
}
263+
}
264+
265+
// copy files to import folder
266+
if (unzippedFiles.Any())
267+
{
268+
using (_rwLock.GetWriteLock())
269+
{
270+
if (deleteExisting)
271+
{
272+
FileSystemHelper.ClearDirectory(importFolder, false);
273+
}
274+
275+
foreach (var file in unzippedFiles)
276+
{
277+
var import = new UploadImportFile(file.Headers);
278+
var destPath = Path.Combine(importFolder, import.FileName);
279+
280+
import.Exists = File.Exists(destPath);
281+
282+
switch (profile.FileType)
283+
{
284+
case ImportFileType.XLSX:
285+
import.IsSupportedByProfile = import.FileExtension.IsCaseInsensitiveEqual(".xlsx");
286+
break;
287+
case ImportFileType.CSV:
288+
import.IsSupportedByProfile = csvTypes.Contains(import.FileExtension, StringComparer.OrdinalIgnoreCase);
289+
break;
290+
}
291+
292+
import.Inserted = FileSystemHelper.Copy(file.LocalFileName, destPath);
293+
294+
result.Add(import);
295+
}
296+
}
297+
}
298+
299+
FileSystemHelper.ClearDirectory(tempDir, true);
300+
return result.AsQueryable();
301+
}
160302
}
161303
}

src/Plugins/SmartStore.WebApi/Description.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
FriendlyName: SmartStore.NET Web Api
22
SystemName: SmartStore.WebApi
3-
Version: 2.2.0.4
3+
Version: 2.2.0.5
44
Group: Api
55
MinAppVersion: 2.2.0
66
Author: SmartStore AG
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System.Collections.Generic;
2+
using System.IO;
3+
using System.Net.Http.Headers;
4+
using System.Runtime.Serialization;
5+
6+
namespace SmartStore.WebApi.Models.Api
7+
{
8+
[DataContract]
9+
public abstract partial class UploadFileBase
10+
{
11+
public UploadFileBase()
12+
{
13+
}
14+
15+
public UploadFileBase(HttpContentHeaders headers)
16+
{
17+
Name = headers.ContentDisposition.Name.ToUnquoted();
18+
FileName = headers.ContentDisposition.FileName.ToUnquoted();
19+
ContentDisposition = headers.ContentDisposition.Parameters;
20+
21+
if (headers.ContentType != null)
22+
{
23+
MediaType = headers.ContentType.MediaType.ToUnquoted();
24+
}
25+
26+
if (FileName.HasValue())
27+
{
28+
FileExtension = Path.GetExtension(FileName);
29+
}
30+
}
31+
32+
/// <summary>
33+
/// Unquoted name attribute of content-disposition multipart header
34+
/// </summary>
35+
[DataMember]
36+
public string Name { get; set; }
37+
38+
/// <summary>
39+
/// Unquoted filename attribute of content-disposition multipart header
40+
/// </summary>
41+
[DataMember]
42+
public string FileName { get; set; }
43+
44+
/// <summary>
45+
/// Extension of FileName
46+
/// </summary>
47+
[DataMember]
48+
public string FileExtension { get; set; }
49+
50+
/// <summary>
51+
/// Media (mime) type of content-type multipart header
52+
/// </summary>
53+
[DataMember]
54+
public string MediaType { get; set; }
55+
56+
/// <summary>
57+
/// Indicates whether the uploaded file already exist
58+
/// </summary>
59+
[DataMember]
60+
public bool Exists { get; set; }
61+
62+
/// <summary>
63+
/// Indicates whether the uploaded file has been inserted
64+
/// </summary>
65+
[DataMember]
66+
public bool Inserted { get; set; }
67+
68+
/// <summary>
69+
/// Raw custom parameters of the content-disposition multipart header
70+
/// </summary>
71+
[DataMember]
72+
public ICollection<NameValueHeaderValue> ContentDisposition { get; set; }
73+
}
74+
}

0 commit comments

Comments
 (0)