diff --git a/zero.Core/Api/ApiBase.cs b/zero.Core/Api/ApiBase.cs
index 91816a93..4fcc2edc 100644
--- a/zero.Core/Api/ApiBase.cs
+++ b/zero.Core/Api/ApiBase.cs
@@ -122,7 +122,7 @@ namespace zero.Core.Api
}
// update name alias
- model.Alias = Alias.Generate(model.Name);
+ model.Alias = Safenames.Alias(model.Name);
using (IAsyncDocumentSession session = Raven.OpenAsyncSession())
{
diff --git a/zero.Core/Api/BackofficeApi.cs b/zero.Core/Api/BackofficeApi.cs
index d644e727..cc782638 100644
--- a/zero.Core/Api/BackofficeApi.cs
+++ b/zero.Core/Api/BackofficeApi.cs
@@ -164,7 +164,7 @@ namespace zero.Core.Api
// update name alias
if (zeroEntity != null)
{
- zeroEntity.Alias = Alias.Generate(zeroEntity.Name);
+ zeroEntity.Alias = Safenames.Alias(zeroEntity.Name);
}
using (IAsyncDocumentSession session = Raven.OpenAsyncSession())
diff --git a/zero.Core/Api/MediaApi.cs b/zero.Core/Api/MediaApi.cs
index 1a6bfff2..b0c7d96b 100644
--- a/zero.Core/Api/MediaApi.cs
+++ b/zero.Core/Api/MediaApi.cs
@@ -35,7 +35,8 @@ namespace zero.Core.Api
///
public async Task> Save(Media model)
{
- return await Save(model);
+ model.IsActive = true;
+ return await SaveModel(model);
}
diff --git a/zero.Core/Api/MediaUploadApi.cs b/zero.Core/Api/MediaUploadApi.cs
new file mode 100644
index 00000000..3aa47981
--- /dev/null
+++ b/zero.Core/Api/MediaUploadApi.cs
@@ -0,0 +1,118 @@
+using Microsoft.AspNetCore.Http;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using zero.Core.Entities;
+using zero.Core.Extensions;
+
+namespace zero.Core.Api
+{
+ public class MediaUploadApi : IMediaUploadApi
+ {
+ protected IPaths Paths { get; set; }
+
+ private const char PATH_SEPARATOR = '/';
+
+ private const string PATH_PREFIX = "/uploads";
+
+ private const char SEP = ',';
+
+ private const string THUMB_EXTENSION = ".thumb";
+
+ private const string PREVIEW_EXTENSION = ".preview";
+
+ private const string UPLOAD_PREFIX = "upload:";
+
+ private string[] ImageExtensions = new[] { ".jpg", ".jpeg", ".png", ".bmp", ".webp", ".jfif" };
+
+
+ public MediaUploadApi(IPaths paths)
+ {
+ Paths = paths;
+ }
+
+
+ ///
+ public async Task Upload(IFormFile file, string folderId, CancellationToken cancellationToken = default)
+ {
+ Media media = new Media();
+
+ // generate file id which is used as the folder name on disk
+ media.FileId = Guid.NewGuid().ToString();
+ media.FolderId = folderId;
+
+ // generate file name
+ media.Name = Safenames.File(file.FileName);
+
+ // build folder and full file path
+ string folderPath = Path.Combine(Paths.Media, media.FileId);
+ string filePath = Path.Combine(folderPath, media.Name);
+ string extension = Path.GetExtension(filePath);
+
+ // find media type
+ media.Type = ImageExtensions.Contains(extension, StringComparer.InvariantCultureIgnoreCase) ? MediaType.Image : MediaType.File;
+
+ Paths.Create(folderPath);
+
+ // write media file to disk
+ using (var stream = File.Create(filePath))
+ {
+ await file.CopyToAsync(stream, cancellationToken);
+ }
+
+ // write additional image data + thumbnail
+ if (media.Type == MediaType.Image)
+ {
+ using (Image image = Image.Load(filePath))
+ {
+ media.Dimension = new MediaDimension()
+ {
+ Width = image.Width,
+ Height = image.Height
+ };
+
+ image.Mutate(x => x.Resize(new ResizeOptions()
+ {
+ Size = new Size(210, 210),
+ Mode = ResizeMode.Min
+ }));
+
+ string thumbFileName = media.Name.TrimEnd(extension) + PREVIEW_EXTENSION + extension;
+ image.Save(Path.Combine(Paths.Media, media.FileId, thumbFileName));
+ media.PreviewSource = Path.Combine(PATH_PREFIX, media.FileId, thumbFileName).Replace(Path.DirectorySeparatorChar, PATH_SEPARATOR);
+
+ image.Mutate(x => x.Resize(new ResizeOptions()
+ {
+ Size = new Size(100, 100),
+ Mode = ResizeMode.Max
+ }));
+
+ thumbFileName = media.Name.TrimEnd(extension) + THUMB_EXTENSION + extension;
+ image.Save(Path.Combine(Paths.Media, media.FileId, thumbFileName));
+ media.ThumbnailSource = Path.Combine(PATH_PREFIX, media.FileId, thumbFileName).Replace(Path.DirectorySeparatorChar, PATH_SEPARATOR);
+ }
+ }
+
+ // set new properties
+ media.LastModifiedDate = DateTimeOffset.Now;
+ media.Source = Path.Combine(PATH_PREFIX, media.FileId, media.Name).Replace(Path.DirectorySeparatorChar, PATH_SEPARATOR);
+ media.Size = file.Length;
+
+ return media;
+ }
+ }
+
+
+ public interface IMediaUploadApi
+ {
+ ///
+ /// Uploads a file to the media folder
+ ///
+ Task Upload(IFormFile file, string folderId, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/zero.Core/Api/Alias.cs b/zero.Core/Api/Safenames.cs
similarity index 76%
rename from zero.Core/Api/Alias.cs
rename to zero.Core/Api/Safenames.cs
index 98370e33..b4ab06cd 100644
--- a/zero.Core/Api/Alias.cs
+++ b/zero.Core/Api/Safenames.cs
@@ -1,22 +1,49 @@
using System;
+using System.IO;
using System.Text;
using zero.Core.Extensions;
namespace zero.Core.Api
{
- public class Alias
+ public class Safenames
{
- private const char HYPHEN = '-';
+ public enum Scope
+ {
+ Url,
+ File
+ }
- private const char PLUS = '+';
+ const char HYPHEN = '-';
- private const char AMPERSAND = '&';
+ const char DOT = '.';
+
+ const char PLUS = '+';
+
+ const char AMPERSAND = '&';
+
+
+ ///
+ /// Converts an untrusted to a safe filename
+ ///
+ public static string File(string value)
+ {
+ return Generate(Path.GetFileName(value), Scope.File);
+ }
///
/// Converts a term to a safe alias (suitable for URLs)
///
- public static string Generate(string value)
+ public static string Alias(string value)
+ {
+ return Generate(value, Scope.Url);
+ }
+
+
+ ///
+ ///
+ ///
+ static string Generate(string value, Scope scope)
{
if (String.IsNullOrWhiteSpace(value))
{
@@ -58,6 +85,10 @@ namespace zero.Core.Api
{
target = PLUS;
}
+ else if (scope == Scope.File && character == DOT)
+ {
+ target = DOT;
+ }
// add hyphen for all other characters
else
{
diff --git a/zero.Core/Api/SetupApi.cs b/zero.Core/Api/SetupApi.cs
index 0f9ec01d..08ea604d 100644
--- a/zero.Core/Api/SetupApi.cs
+++ b/zero.Core/Api/SetupApi.cs
@@ -64,7 +64,7 @@ namespace zero.Core.Api
CreatedDate = DateTimeOffset.Now,
IsActive = true,
Name = model.AppName,
- Alias = Alias.Generate(model.AppName)
+ Alias = Safenames.Alias(model.AppName)
};
// create user
@@ -76,7 +76,7 @@ namespace zero.Core.Api
Name = model.User.Name,
IsActive = true,
LanguageId = Options.DefaultLanguage,
- Alias = Alias.Generate(model.User.Name),
+ Alias = Safenames.Alias(model.User.Name),
IsEmailConfirmed = true
};
@@ -84,7 +84,7 @@ namespace zero.Core.Api
Language language = new Language() // TODO get default language selection from setup UI
{
Name = "English",
- Alias = Alias.Generate("English"),
+ Alias = Safenames.Alias("English"),
CreatedDate = DateTimeOffset.Now,
Code = "en-US",
IsActive = true,
@@ -209,7 +209,7 @@ namespace zero.Core.Api
{
CreatedDate = DateTimeOffset.Now,
IsActive = true,
- Alias = Alias.Generate(country.Value),
+ Alias = Safenames.Alias(country.Value),
LanguageId = defaultLanguage.Id,
Code = country.Key.ToLowerInvariant(),
Name = country.Value
@@ -230,7 +230,7 @@ namespace zero.Core.Api
UserRole adminRole = new UserRole()
{
Name = "Administrator",
- Alias = Alias.Generate("Administrator"),
+ Alias = Safenames.Alias("Administrator"),
Sort = 0,
AppId = Constants.Database.SharedAppId,
Icon = "fth-award",
@@ -256,7 +256,7 @@ namespace zero.Core.Api
UserRole editorRole = new UserRole()
{
Name = "Editor",
- Alias = Alias.Generate("Editor"),
+ Alias = Safenames.Alias("Editor"),
Sort = 1,
AppId = Constants.Database.SharedAppId,
Icon = "fth-feather",
@@ -276,7 +276,7 @@ namespace zero.Core.Api
UserRole defaultRole = new UserRole()
{
Name = "Standard",
- Alias = Alias.Generate("Standard"),
+ Alias = Safenames.Alias("Standard"),
Sort = 2,
AppId = Constants.Database.SharedAppId,
Icon = "fth-users",
diff --git a/zero.Core/Api/SpacesApi.cs b/zero.Core/Api/SpacesApi.cs
index dd47bec7..e11065d8 100644
--- a/zero.Core/Api/SpacesApi.cs
+++ b/zero.Core/Api/SpacesApi.cs
@@ -144,7 +144,7 @@ namespace zero.Core.Api
}
model.SpaceAlias = alias;
- model.Alias = Alias.Generate(model.Name);
+ model.Alias = Safenames.Alias(model.Name);
using (IAsyncDocumentSession session = Raven.OpenAsyncSession())
{
diff --git a/zero.Core/Api/UserRolesApi.cs b/zero.Core/Api/UserRolesApi.cs
index d524efbb..52915831 100644
--- a/zero.Core/Api/UserRolesApi.cs
+++ b/zero.Core/Api/UserRolesApi.cs
@@ -67,7 +67,7 @@ namespace zero.Core.Api
model.CreatedDate = DateTimeOffset.Now;
}
- model.Alias = Alias.Generate(model.Name);
+ model.Alias = Safenames.Alias(model.Name);
using (IAsyncDocumentSession session = Raven.OpenAsyncSession())
{
diff --git a/zero.Core/Entities/Media/Media.cs b/zero.Core/Entities/Media/Media.cs
index af845b57..77a57de1 100644
--- a/zero.Core/Entities/Media/Media.cs
+++ b/zero.Core/Entities/Media/Media.cs
@@ -1,4 +1,5 @@
using System;
+using zero.Core.Attributes;
namespace zero.Core.Entities
{
@@ -29,7 +30,10 @@ namespace zero.Core.Entities
public string ThumbnailSource { get; set; }
///
- public int Size { get; set; }
+ public string PreviewSource { get; set; }
+
+ ///
+ public long Size { get; set; }
///
public MediaDimension Dimension { get; set; }
@@ -39,13 +43,17 @@ namespace zero.Core.Entities
///
public MediaFocalPoint FocalPoint { get; set; }
+
+ ///
+ public MediaType Type { get; set; }
}
+ [Collection("Media")]
public interface IMedia : IZeroEntity, IAppAwareEntity, IZeroDbConventions
{
///
- /// Id/name of the folder which is stored on disk/cloud
+ /// Id/name of the phyiscal folder which is stored on disk/cloud
///
public string FileId { get; set; }
@@ -74,10 +82,15 @@ namespace zero.Core.Entities
///
string ThumbnailSource { get; set; }
+ ///
+ /// For images this is the source for a [proportional]x210px thumbnail
+ ///
+ string PreviewSource { get; set; }
+
///
/// Filesize in bytes
///
- int Size { get; set; }
+ long Size { get; set; }
///
/// Dimension (width + height) in pixels
@@ -93,5 +106,10 @@ namespace zero.Core.Entities
/// Optional focal point for an image
///
MediaFocalPoint FocalPoint { get; set; }
+
+ ///
+ /// Type of the media
+ ///
+ MediaType Type { get; set; }
}
}
diff --git a/zero.Core/Entities/Media/MediaType.cs b/zero.Core/Entities/Media/MediaType.cs
new file mode 100644
index 00000000..b33ff27b
--- /dev/null
+++ b/zero.Core/Entities/Media/MediaType.cs
@@ -0,0 +1,8 @@
+namespace zero.Core.Entities
+{
+ public enum MediaType
+ {
+ File = 0,
+ Image = 1
+ }
+}
diff --git a/zero.Core/Identity/RoleStore.cs b/zero.Core/Identity/RoleStore.cs
index 11c1df16..2a08a4fc 100644
--- a/zero.Core/Identity/RoleStore.cs
+++ b/zero.Core/Identity/RoleStore.cs
@@ -93,7 +93,7 @@ namespace zero.Core.Identity
///
public Task SetRoleNameAsync(TRole role, string roleName, CancellationToken cancellationToken)
{
- role.Alias = Alias.Generate(roleName);
+ role.Alias = Safenames.Alias(roleName);
return Task.CompletedTask;
}
diff --git a/zero.Web.UI/App/pages/media/media.vue b/zero.Web.UI/App/pages/media/media.vue
index e04b67dd..cf56e18b 100644
--- a/zero.Web.UI/App/pages/media/media.vue
+++ b/zero.Web.UI/App/pages/media/media.vue
@@ -34,8 +34,8 @@
-
- {{item.source}}
+
+ {{item.name}}
@@ -265,14 +265,18 @@
name: file.name,
size: file.size,
mimeType: file.type,
- source: null,
- file: file
+ source: null
});
+
//items.push(this.addFile(files[i]));
+
+ MediaApi.upload(file, this.id).then(res =>
+ {
+ console.info(res);
+ });
}
}
- console.info(items);
//this.update();
},
diff --git a/zero.Web.UI/App/resources/media.js b/zero.Web.UI/App/resources/media.js
index 2d485031..028148c7 100644
--- a/zero.Web.UI/App/resources/media.js
+++ b/zero.Web.UI/App/resources/media.js
@@ -28,6 +28,27 @@ export default {
return Axios.post(base + 'save', model).then(res => Promise.resolve(res.data));
},
+ // uploads a file
+ upload(file, folderId)
+ {
+ var data = new FormData();
+ data.append('file', file);
+ data.append('folderId', folderId);
+
+ return Axios.post(base + 'upload', data, {
+ onUploadProgress: function (progressEvent)
+ {
+ var percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
+ console.info('upload: ' + percentCompleted);
+ }
+ }).then(function (res)
+ {
+ Promise.resolve(res.data);
+ });
+
+ //return Axios.post(base + 'save', model).then(res => Promise.resolve(res.data));
+ },
+
// deletes a media
delete(id)
{
diff --git a/zero.Web/Controllers/MediaController.cs b/zero.Web/Controllers/MediaController.cs
index cbcce8b1..bec65807 100644
--- a/zero.Web/Controllers/MediaController.cs
+++ b/zero.Web/Controllers/MediaController.cs
@@ -1,4 +1,5 @@
-using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -18,11 +19,14 @@ namespace zero.Web.Controllers
IMediaFolderApi MediaFolderApi;
+ IMediaUploadApi MediaUploadApi;
- public MediaController(IMediaApi api, IMediaFolderApi mediaFolderApi)
+
+ public MediaController(IMediaApi api, IMediaFolderApi mediaFolderApi, IMediaUploadApi mediaUploadApi)
{
Api = api;
MediaFolderApi = mediaFolderApi;
+ MediaUploadApi = mediaUploadApi;
}
@@ -68,6 +72,16 @@ namespace zero.Web.Controllers
}
+ ///
+ /// Upload a file
+ ///
+ public async Task Upload(IFormFile file, string folderId)
+ {
+ Media media = await MediaUploadApi.Upload(file, folderId);
+ return Json(await Api.Save(media));
+ }
+
+
///
/// Deletes a media item
///
diff --git a/zero.Web/Controllers/SettingsController.cs b/zero.Web/Controllers/SettingsController.cs
index 1b2e9c1b..59d4bbaa 100644
--- a/zero.Web/Controllers/SettingsController.cs
+++ b/zero.Web/Controllers/SettingsController.cs
@@ -52,7 +52,7 @@ namespace zero.Web.Controllers
Name = area.Name,
Description = area.Description,
Icon = area.Icon,
- Url = Constants.Sections.Settings.EnsureStartsWith('/') + Alias.Generate(area.Alias).EnsureStartsWith('/')
+ Url = Constants.Sections.Settings.EnsureStartsWith('/') + Safenames.Alias(area.Alias).EnsureStartsWith('/')
};
areas.Add(vueArea);
diff --git a/zero.Web/Defaults/ZeroBackofficePlugin.cs b/zero.Web/Defaults/ZeroBackofficePlugin.cs
index 11e40162..041ad37c 100644
--- a/zero.Web/Defaults/ZeroBackofficePlugin.cs
+++ b/zero.Web/Defaults/ZeroBackofficePlugin.cs
@@ -48,6 +48,7 @@ namespace zero.Web.Defaults
services.AddTransient();
services.AddTransient();
services.AddTransient();
+ services.AddTransient();
services.AddTransient();
}
diff --git a/zero.Web/Mapper/MediaMapperConfig.cs b/zero.Web/Mapper/MediaMapperConfig.cs
index 53d52ea5..ee83104e 100644
--- a/zero.Web/Mapper/MediaMapperConfig.cs
+++ b/zero.Web/Mapper/MediaMapperConfig.cs
@@ -38,11 +38,11 @@ namespace zero.Web.Mapper
config.CreateMap((source, target) =>
{
target.Id = source.Id;
- target.Name = source.Name;
target.IsFolder = false;
+ target.Name = source.Name;
+ target.Type = source.Type;
target.Size = source.Size;
- target.Source = source.Source;
- target.ThumbnailSource = source.ThumbnailSource;
+ target.Source = source.PreviewSource ?? source.Source;
});
config.CreateMap((source, target) =>
diff --git a/zero.Web/Models/MediaListModel.cs b/zero.Web/Models/MediaListModel.cs
index 7058e2ce..22f94791 100644
--- a/zero.Web/Models/MediaListModel.cs
+++ b/zero.Web/Models/MediaListModel.cs
@@ -1,15 +1,19 @@
-namespace zero.Web.Models
+using zero.Core.Entities;
+
+namespace zero.Web.Models
{
public class MediaListModel : ListModel
{
public bool IsFolder { get; set; }
+ public MediaType Type { get; set; }
+
public string Name { get; set; }
public string Source { get; set; }
public string ThumbnailSource { get; set; }
- public int Size { get; set; }
+ public long Size { get; set; }
}
}
diff --git a/zero.Web/ZeroVue.cs b/zero.Web/ZeroVue.cs
index 23d1eb29..c627ec75 100644
--- a/zero.Web/ZeroVue.cs
+++ b/zero.Web/ZeroVue.cs
@@ -97,7 +97,7 @@ namespace zero.Web
}
bool isExternal = !(section is IZeroInternal);
- string url = Alias.Generate(section.Alias).EnsureStartsWith('/');
+ string url = Safenames.Alias(section.Alias).EnsureStartsWith('/');
if (section.Alias == Constants.Sections.Dashboard)
{
@@ -121,7 +121,7 @@ namespace zero.Web
{
Alias = child.Alias,
Name = child.Name,
- Url = vueSection.Url.EnsureEndsWith('/') + Alias.Generate(child.Alias),
+ Url = vueSection.Url.EnsureEndsWith('/') + Safenames.Alias(child.Alias),
IsExternal = isExternal
});
}
@@ -192,7 +192,7 @@ namespace zero.Web
Name = area.Name,
Description = area.Description,
Icon = area.Icon,
- Url = Constants.Sections.Settings.EnsureStartsWith('/') + Alias.Generate(area.Alias).EnsureStartsWith('/'),
+ Url = Constants.Sections.Settings.EnsureStartsWith('/') + Safenames.Alias(area.Alias).EnsureStartsWith('/'),
IsPlugin = isPlugin
};