upload media files!
This commit is contained in:
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -35,7 +35,8 @@ namespace zero.Core.Api
|
||||
/// <inheritdoc />
|
||||
public async Task<EntityResult<Media>> Save(Media model)
|
||||
{
|
||||
return await Save(model);
|
||||
model.IsActive = true;
|
||||
return await SaveModel(model);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Media> 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<Rgba32> image = Image.Load<Rgba32>(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
|
||||
{
|
||||
/// <summary>
|
||||
/// Uploads a file to the media folder
|
||||
/// </summary>
|
||||
Task<Media> Upload(IFormFile file, string folderId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
@@ -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 = '&';
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts an untrusted to a safe filename
|
||||
/// </summary>
|
||||
public static string File(string value)
|
||||
{
|
||||
return Generate(Path.GetFileName(value), Scope.File);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Converts a term to a safe alias (suitable for URLs)
|
||||
/// </summary>
|
||||
public static string Generate(string value)
|
||||
public static string Alias(string value)
|
||||
{
|
||||
return Generate(value, Scope.Url);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
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
|
||||
{
|
||||
@@ -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",
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Size { get; set; }
|
||||
public string PreviewSource { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public long Size { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public MediaDimension Dimension { get; set; }
|
||||
@@ -39,13 +43,17 @@ namespace zero.Core.Entities
|
||||
|
||||
/// <inheritdoc />
|
||||
public MediaFocalPoint FocalPoint { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public MediaType Type { get; set; }
|
||||
}
|
||||
|
||||
|
||||
[Collection("Media")]
|
||||
public interface IMedia : IZeroEntity, IAppAwareEntity, IZeroDbConventions
|
||||
{
|
||||
/// <summary>
|
||||
/// Id/name of the folder which is stored on disk/cloud
|
||||
/// Id/name of the phyiscal folder which is stored on disk/cloud
|
||||
/// </summary>
|
||||
public string FileId { get; set; }
|
||||
|
||||
@@ -74,10 +82,15 @@ namespace zero.Core.Entities
|
||||
/// </summary>
|
||||
string ThumbnailSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For images this is the source for a [proportional]x210px thumbnail
|
||||
/// </summary>
|
||||
string PreviewSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Filesize in bytes
|
||||
/// </summary>
|
||||
int Size { get; set; }
|
||||
long Size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Dimension (width + height) in pixels
|
||||
@@ -93,5 +106,10 @@ namespace zero.Core.Entities
|
||||
/// Optional focal point for an image
|
||||
/// </summary>
|
||||
MediaFocalPoint FocalPoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of the media
|
||||
/// </summary>
|
||||
MediaType Type { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace zero.Core.Entities
|
||||
{
|
||||
public enum MediaType
|
||||
{
|
||||
File = 0,
|
||||
Image = 1
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,7 @@ namespace zero.Core.Identity
|
||||
/// <inheritdoc/>
|
||||
public Task SetRoleNameAsync(TRole role, string roleName, CancellationToken cancellationToken)
|
||||
{
|
||||
role.Alias = Alias.Generate(roleName);
|
||||
role.Alias = Safenames.Alias(roleName);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@
|
||||
<router-link :to="getLink(item, true)" class="media-item" v-for="item in items" :key="item.id">
|
||||
<img v-if="item.type === 'image'" :src="item.source" />
|
||||
<span class="media-item-content is-file" v-if="item.type !== 'image'">
|
||||
<i :class="icons[item.type]" :data-extension="item.extension"></i>
|
||||
<span>{{item.source}}</span>
|
||||
<i :class="icons[item.type]" :data-extension="item.source.split('.').pop()"></i>
|
||||
<span>{{item.name}}</span>
|
||||
</span>
|
||||
</router-link>
|
||||
</div>
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Upload a file
|
||||
/// </summary>
|
||||
public async Task<IActionResult> Upload(IFormFile file, string folderId)
|
||||
{
|
||||
Media media = await MediaUploadApi.Upload(file, folderId);
|
||||
return Json(await Api.Save(media));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a media item
|
||||
/// </summary>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -48,6 +48,7 @@ namespace zero.Web.Defaults
|
||||
services.AddTransient<IPermissionsApi, PermissionsApi>();
|
||||
services.AddTransient<IMediaApi, MediaApi>();
|
||||
services.AddTransient<IMediaFolderApi, MediaFolderApi>();
|
||||
services.AddTransient<IMediaUploadApi, MediaUploadApi>();
|
||||
services.AddTransient<IMediaUpload, MediaUpload>();
|
||||
}
|
||||
|
||||
|
||||
@@ -38,11 +38,11 @@ namespace zero.Web.Mapper
|
||||
config.CreateMap<Media, MediaListModel>((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<MediaFolder, MediaListModel>((source, target) =>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user