upload media files!

This commit is contained in:
2020-05-31 15:01:28 +02:00
parent 7576687fdd
commit 8d2c8b1734
19 changed files with 257 additions and 37 deletions
+1 -1
View File
@@ -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())
{
+1 -1
View File
@@ -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())
+2 -1
View File
@@ -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);
}
+118
View File
@@ -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
{
+7 -7
View File
@@ -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",
+1 -1
View File
@@ -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())
{
+1 -1
View File
@@ -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())
{
+21 -3
View File
@@ -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; }
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace zero.Core.Entities
{
public enum MediaType
{
File = 0,
Image = 1
}
}
+1 -1
View File
@@ -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;
}
+9 -5
View File
@@ -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();
},
+21
View File
@@ -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)
{
+16 -2
View File
@@ -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>
+1 -1
View File
@@ -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>();
}
+3 -3
View File
@@ -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) =>
+6 -2
View File
@@ -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
View File
@@ -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
};