diff --git a/Server/Controller/BaseController.cs b/Server/Controller/BaseController.cs
index 087a324..403f6cc 100644
--- a/Server/Controller/BaseController.cs
+++ b/Server/Controller/BaseController.cs
@@ -1,7 +1,9 @@
using Blahblah.FlowerStory.Server.Data;
using Blahblah.FlowerStory.Server.Data.Model;
using Microsoft.AspNetCore.Mvc;
+using SkiaSharp;
using System.Net;
+using System.Reflection;
using System.Security.Cryptography;
using System.Text;
@@ -14,6 +16,8 @@ public abstract partial class BaseController : ControllerBase
{
private const string Salt = "Blah blah, o! Flower story, intimately help you record every bit of the garden.";
+ private const int ThumbWidth = 600;
+
///
/// 临时 id
///
@@ -23,7 +27,7 @@ public abstract partial class BaseController : ControllerBase
///
/// 文件上传路径
///
- protected static string UploadsDirectory => uploadsDirectory ??= Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads");
+ protected static string UploadsDirectory => uploadsDirectory ??= Path.Combine(Program.DataPath, "uploads");
///
/// 支持的图片文件签名
@@ -31,6 +35,7 @@ public abstract partial class BaseController : ControllerBase
protected static readonly List PhotoSignatures = new()
{
// jpeg
+ new byte[] { 0xFF, 0xD8, 0xFF, 0xDB },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE1 },
new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
@@ -195,6 +200,29 @@ public abstract partial class BaseController : ControllerBase
return count;
}
+ ///
+ /// 获取嵌入流的数据
+ ///
+ /// 文件名
+ /// 命名空间,默认为程序集.wwwroot
+ ///
+ protected static byte[] GetEmbeddedData(string filename, string? @namespace = null)
+ {
+ Assembly asm = Assembly.GetExecutingAssembly();
+ if (string.IsNullOrEmpty(@namespace))
+ {
+ @namespace = typeof(Program).Namespace + ".wwwroot";
+ }
+ using Stream? stream = asm.GetManifestResourceStream($"{@namespace}.{filename}");
+ if (stream == null)
+ {
+ return Array.Empty();
+ }
+ using var ms = new MemoryStream();
+ stream.CopyTo(ms);
+ return ms.ToArray();
+ }
+
///
/// 读取文件到 byte 数组
///
@@ -230,7 +258,7 @@ public abstract partial class BaseController : ControllerBase
var ext = Path.GetExtension(name);
var path = $"{WebUtility.UrlEncode(Path.GetFileNameWithoutExtension(name))}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}{ext}";
- var image = SkiaSharp.SKImage.FromEncodedData(data);
+ var image = SKImage.FromEncodedData(data);
return new FileResult
{
@@ -256,17 +284,57 @@ public abstract partial class BaseController : ControllerBase
return directory;
}
+ ///
+ /// 创建缩略图
+ ///
+ /// 图片数据
+ /// 最大宽度
+ /// 缩放质量
+ ///
+ protected static byte[] CreateThumbnail(byte[] data, int maxWidth = ThumbWidth, SKFilterQuality quality = SKFilterQuality.Medium)
+ {
+ if (maxWidth <= 0)
+ {
+ return data;
+ }
+ using var bitmap = SKBitmap.Decode(data);
+ if (maxWidth >= bitmap.Width)
+ {
+ using var enc = bitmap.Encode(SKEncodedImageFormat.Jpeg, 80);
+ return enc.ToArray();
+ }
+ var height = maxWidth * bitmap.Height / bitmap.Width;
+ using var image = bitmap.Resize(new SKImageInfo(maxWidth, height), quality);
+ using var encode = image.Encode(SKEncodedImageFormat.Jpeg, 80);
+ return encode.ToArray();
+ }
+
///
/// 写入文件到用户的花草目录中
///
/// 花草唯一 id
/// 文件对象
+ /// 创建缩略图
/// 取消令牌
- protected static async Task WriteToFile(int fid, FileResult file, CancellationToken token = default)
+ protected async Task WriteToFile(int fid, FileResult file, bool thumbnail = true, CancellationToken token = default)
{
var directory = GetFlowerDirectory(fid, true);
var path = Path.Combine(directory, file.Path);
await System.IO.File.WriteAllBytesAsync(path, file.Content, token);
+
+ if (thumbnail)
+ {
+ try
+ {
+ var thumb = CreateThumbnail(file.Content);
+ path = Path.Combine(directory, $"{file.Path}.thumb");
+ await System.IO.File.WriteAllBytesAsync(path, thumb, token);
+ }
+ catch (Exception ex)
+ {
+ logger?.LogError(ex, "failed to create thumbnail for flower: {fid}, file: {file}, error: {message}", fid, file.Filename, ex.Message);
+ }
+ }
}
///
diff --git a/Server/Controller/BaseController.sqlite.cs b/Server/Controller/BaseController.sqlite.cs
index fe9bb7c..9af40fd 100644
--- a/Server/Controller/BaseController.sqlite.cs
+++ b/Server/Controller/BaseController.sqlite.cs
@@ -52,7 +52,7 @@ partial class BaseController
}
///
- /// 获取用户头像数据
+ /// 根据用户 uid 获取用户头像数据
///
/// 用户唯一 id
///
@@ -61,6 +61,16 @@ partial class BaseController
return database.Database.SqlQuery($"SELECT \"avatar\" AS \"Value\" FROM \"users\" WHERE \"uid\" = {uid} LIMIT 1").SingleOrDefault();
}
+ ///
+ /// 根据用户 id 获取用户头像数据
+ ///
+ /// 用户 id
+ ///
+ protected byte[]? QueryUserAvatar(string id)
+ {
+ return database.Database.SqlQuery($"SELECT \"avatar\" AS \"Value\" FROM \"users\" WHERE \"id\" = {id} LIMIT 1").SingleOrDefault();
+ }
+
///
/// 移除用户头像
///
diff --git a/Server/Controller/EventApiController.cs b/Server/Controller/EventApiController.cs
index 566580e..a44d86c 100644
--- a/Server/Controller/EventApiController.cs
+++ b/Server/Controller/EventApiController.cs
@@ -316,7 +316,7 @@ public class EventApiController : BaseController
};
AddPhotoItem(p);
- await WriteToFile(@event.FlowerId, file, token);
+ await WriteToFile(@event.FlowerId, file, token: token);
}
});
}
@@ -443,7 +443,7 @@ public class EventApiController : BaseController
};
AddPhotoItem(cover);
- await WriteToFile(update.FlowerId, file, token);
+ await WriteToFile(update.FlowerId, file, token: token);
}
});
}
@@ -541,7 +541,7 @@ public class EventApiController : BaseController
};
AddPhotoItem(p);
- await WriteToFile(record.FlowerId, file, token);
+ await WriteToFile(record.FlowerId, file, token: token);
});
}
catch (Exception ex)
@@ -639,7 +639,7 @@ public class EventApiController : BaseController
};
AddPhotoItem(p);
- await WriteToFile(record.FlowerId, file, token);
+ await WriteToFile(record.FlowerId, file, token: token);
}
}
});
diff --git a/Server/Controller/FlowerApiController.cs b/Server/Controller/FlowerApiController.cs
index 24b4edb..1ab3a52 100644
--- a/Server/Controller/FlowerApiController.cs
+++ b/Server/Controller/FlowerApiController.cs
@@ -235,7 +235,7 @@ public class FlowerApiController : BaseController
f.Photos = database.Photos.Where(p => p.FlowerId == f.Id && p.RecordId == null).ToList();
foreach (var photo in f.Photos)
{
- photo.Url = $"photo/flower/{f.Id}/{photo.Path}";
+ photo.Url = $"photo/flower/{f.Id}/{photo.Path}?thumb=1";
}
}
}
@@ -532,7 +532,7 @@ public class FlowerApiController : BaseController
};
AddPhotoItem(cover);
- await WriteToFile(item.Id, file, token);
+ await WriteToFile(item.Id, file, token: token);
});
}
catch (Exception ex)
@@ -753,7 +753,7 @@ public class FlowerApiController : BaseController
};
AddPhotoItem(cover);
- await WriteToFile(update.Id, file, token);
+ await WriteToFile(update.Id, file, token: token);
});
}
catch (Exception ex)
@@ -854,7 +854,7 @@ public class FlowerApiController : BaseController
};
AddPhotoItem(cover);
- await WriteToFile(param.Id, file, token);
+ await WriteToFile(param.Id, file, token: token);
});
}
catch (Exception ex)
diff --git a/Server/Controller/ImageController.cs b/Server/Controller/ImageController.cs
index 5ce34a1..2f218ff 100644
--- a/Server/Controller/ImageController.cs
+++ b/Server/Controller/ImageController.cs
@@ -8,35 +8,36 @@ namespace Blahblah.FlowerStory.Server.Controller;
///
/// 图片相关服务
///
-[Produces("image/png")]
[Route("photo")]
public class ImageController : BaseController
{
+ static byte[]? emptyAvatar;
+
+ static byte[] EmptyAvatar => emptyAvatar ??= GetEmbeddedData("image.avatar.jpg");
+
///
public ImageController(FlowerDatabase database, ILogger? logger = null) : base(database, logger)
{
}
///
- /// 请求用户头像
+ /// 请求自己的头像
///
///
/// 请求示例:
///
- /// GET /photo/avatar
+ /// GET /photo/my_avatar
/// Authorization: authorization id
///
///
- /// 认证通过则显示用户头像
+ /// 认证通过则显示自己的头像
/// 返回头像
/// 认证失败
- /// 未找到头像
- [Route("avatar", Name = "getAvatar")]
+ [Route("my_avatar", Name = "getMyAvatar")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpGet]
- public ActionResult GetUserAvatar()
+ public ActionResult GetMyAvatar()
{
var (result, token) = CheckToken();
if (result != null)
@@ -48,11 +49,47 @@ public class ImageController : BaseController
return Unauthorized();
}
var avatar = QueryUserAvatar(token.UserId);
- if (avatar == null)
+ if (avatar?.Length > 0)
{
- return NotFound();
+ return File(avatar, "image/jpeg");
}
- return File(avatar, "image/png");
+ return File(EmptyAvatar, "image/jpeg");
+ }
+
+ ///
+ /// 请求用户头像
+ ///
+ ///
+ /// 请求示例:
+ ///
+ /// GET /photo/avatar/2.jpg
+ ///
+ ///
+ /// 用户唯一 id
+ /// 认证通过则显示用户头像
+ /// 返回头像
+ /// 认证失败
+ [Route("avatar/{uid}.jpg", Name = "getAvatar")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [HttpGet]
+ public ActionResult GetAvatar([Required] int uid)
+ {
+ //var (result, token) = CheckToken();
+ //if (result != null)
+ //{
+ // return result;
+ //}
+ //if (token == null)
+ //{
+ // return Unauthorized();
+ //}
+ var avatar = QueryUserAvatar(uid);
+ if (avatar?.Length > 0)
+ {
+ return File(avatar, "image/jpeg");
+ }
+ return File(EmptyAvatar, "image/jpeg");
}
///
@@ -61,11 +98,12 @@ public class ImageController : BaseController
///
/// 请求示例:
///
- /// GET /photo/flower/{fid}/{name}
+ /// GET /photo/flower/1/test.jpg
///
///
/// 花草唯一 id
/// 照片名称
+ /// 是否为缩略图
/// 认证通过则显示花草照片
/// 返回花草照片
/// 认证失败
@@ -75,7 +113,7 @@ public class ImageController : BaseController
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[HttpGet]
- public async Task GetFlowerPhoto([Required] int fid, [Required] string name)
+ public async Task GetFlowerPhoto([Required] int fid, [Required] string name, [FromQuery] string? thumb = null)
{
//var (result, token) = CheckToken();
//if (result != null)
@@ -87,7 +125,7 @@ public class ImageController : BaseController
// return Unauthorized();
//}
-#if !DEBUG
+#if PRODUCTION
var referrer = Request.Headers.Referer.ToString();
if (string.IsNullOrEmpty(referrer))
{
@@ -98,12 +136,41 @@ public class ImageController : BaseController
return Forbid();
}
#endif
- var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "uploads", fid.ToString(), WebUtility.UrlEncode(name));
- if (System.IO.File.Exists(path))
+ var filename = WebUtility.UrlEncode(name);
+ var directory = Path.Combine(Program.DataPath, "uploads", fid.ToString());
+ var original = Path.Combine(directory, filename);
+ var thumbnail = Path.Combine(directory, $"{filename}.thumb");
+ if (!string.IsNullOrEmpty(thumb))
{
- var data = await System.IO.File.ReadAllBytesAsync(path);
- var ext = Path.GetExtension(path).ToLower();
- return File(data, ext == ".png" ? "image/png" : "image/jpeg");
+ if (System.IO.File.Exists(thumbnail))
+ {
+ var thumbnailData = await System.IO.File.ReadAllBytesAsync(thumbnail);
+ return File(thumbnailData, "image/jpeg");
+ }
+ else if (System.IO.File.Exists(original))
+ {
+ try
+ {
+ var originalData = await System.IO.File.ReadAllBytesAsync(original);
+ var thumbnailData = CreateThumbnail(originalData);
+ await System.IO.File.WriteAllBytesAsync(thumbnail, thumbnailData);
+ return File(thumbnailData, "image/jpeg");
+ }
+ catch (Exception ex)
+ {
+ logger?.LogWarning(ex, "failed to create thumbnail for flower: {fid}, name: {name}, error: {message}", fid, name, ex.Message);
+ }
+ }
+ return NotFound();
+ }
+ if (System.IO.File.Exists(original))
+ {
+ var data = await System.IO.File.ReadAllBytesAsync(original);
+ return Path.GetExtension(original).ToLower() switch
+ {
+ ".png" => File(data, "image/png"),
+ _ => File(data, "image/jpeg"),
+ };
}
return NotFound();
}
diff --git a/Server/Controller/UserApiController.cs b/Server/Controller/UserApiController.cs
index 2136ec4..f531bd6 100644
--- a/Server/Controller/UserApiController.cs
+++ b/Server/Controller/UserApiController.cs
@@ -1,6 +1,7 @@
using Blahblah.FlowerStory.Server.Data;
using Blahblah.FlowerStory.Server.Data.Model;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
using System.Security.Cryptography;
@@ -81,6 +82,9 @@ public partial class UserApiController : BaseController
clientApp = "browser";
expires = 20 * 60; // 20 mins
}
+
+ database.Tokens.Where(t => t.UserId == user.Id && t.ClientApp == clientApp).ExecuteDelete();
+
var now = DateTimeOffset.UtcNow;
var token = new TokenItem
{
@@ -184,26 +188,31 @@ public partial class UserApiController : BaseController
/// 请求示例:
///
/// POST /api/user/register
- /// {
- /// "id": "blahblah",
- /// "password": "pwd123",
- /// "userName": "Blah blah",
- /// "email": "blah@example.com",
- /// "mobile": "18012345678"
- /// }
+ ///
+ /// 参数:
+ ///
+ /// id: "blahblah"
+ /// password: "pwd123"
+ /// name: "Blah blah"
+ /// email: "blah@example.com"
+ /// mobile: "18012345678"
+ /// avatar: <avatar>
///
///
/// 注册参数
/// 成功注册则返回已注册的用户对象
/// 返回已注册的用户对象
+ /// 用户头像格式非法
/// 用户重复或其他服务器错误
[Route("register", Name = "register")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpPost]
- [Consumes("application/json")]
- public ActionResult Register([FromBody] UserParameter user)
+ [Consumes("multipart/form-data")]
+ [RequestSizeLimit(15 * 1024 * 1024)]
+ public ActionResult Register([FromForm] UserParameter user)
{
#if DEBUG
logger?.LogInformation("user register, {user}", user);
@@ -214,6 +223,21 @@ public partial class UserApiController : BaseController
return Problem("duplicateUser", "api/user/register");
}
+ byte[]? data;
+ if (user.Avatar != null)
+ {
+ var avatar = WrapFormFile(user.Avatar);
+ if (avatar == null)
+ {
+ return BadRequest();
+ }
+ data = CreateThumbnail(avatar.Content);
+ }
+ else
+ {
+ data = null;
+ }
+
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var item = new UserItem
{
@@ -224,7 +248,8 @@ public partial class UserApiController : BaseController
ActiveDateUnixTime = now,
Name = user.UserName,
Email = user.Email,
- Mobile = user.Mobile
+ Mobile = user.Mobile,
+ Avatar = data
};
database.Users.Add(item);
SaveDatabase();
@@ -281,30 +306,35 @@ public partial class UserApiController : BaseController
///
/// PUT /api/user/update
/// Authorization: authorization id
- /// {
- /// "userName": "Blah blah",
- /// "email": "blah@example.com",
- /// "mobile": "18012345678"
- /// }
+ ///
+ /// 参数:
+ ///
+ /// name": "Blah blah"
+ /// email": "blah@example.com"
+ /// mobile": "18012345678",
+ /// avatar: <avatar>
///
///
/// 修改参数
/// 修改成功则返回已修改的用户对象
/// 返回已修改的用户对象
+ /// 用户头像格式非法
/// 未找到登录会话或已过期
/// 用户已禁用
/// 未找到关联用户
/// 提交正文过大
[Route("update", Name = "updateProfile")]
[ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
[ProducesErrorResponseType(typeof(ErrorResponse))]
[HttpPut]
- [Consumes("application/json")]
- public ActionResult Update([FromBody] UpdateParameter update)
+ [Consumes("multipart/form-data")]
+ [RequestSizeLimit(15 * 1024 * 1024)]
+ public ActionResult Update([FromForm] UpdateParameter update)
{
#if DEBUG
logger?.LogInformation("user update, {user}", update);
@@ -319,6 +349,17 @@ public partial class UserApiController : BaseController
return NotFound();
}
+ if (update.Avatar != null)
+ {
+ var avatar = WrapFormFile(update.Avatar);
+ if (avatar == null)
+ {
+ return BadRequest();
+ }
+
+ user.Avatar = CreateThumbnail(avatar.Content);
+ }
+
user.Name = update.UserName;
user.Email = update.Email;
user.Mobile = update.Mobile;
@@ -382,7 +423,7 @@ public partial class UserApiController : BaseController
{
return BadRequest();
}
- user.Avatar = file.Content;
+ user.Avatar = CreateThumbnail(file.Content);
}
SaveDatabase();
@@ -429,7 +470,7 @@ public partial class UserApiController : BaseController
return Ok(count);
}
- //#if DEBUG
+#if !PRODUCTION
///
/// #DEBUG 获取所有用户
///
@@ -453,5 +494,5 @@ public partial class UserApiController : BaseController
{
return Ok(database.Tokens.ToArray());
}
- //#endif
+#endif
}
diff --git a/Server/Controller/UserApiController.structs.cs b/Server/Controller/UserApiController.structs.cs
index f5cee42..3bec17a 100644
--- a/Server/Controller/UserApiController.structs.cs
+++ b/Server/Controller/UserApiController.structs.cs
@@ -1,4 +1,5 @@
-using System.ComponentModel.DataAnnotations;
+using Microsoft.AspNetCore.Mvc;
+using System.ComponentModel.DataAnnotations;
namespace Blahblah.FlowerStory.Server.Controller;
@@ -33,12 +34,14 @@ public record UserParameter : UpdateParameter
/// 用户 id
///
[Required]
+ [FromForm(Name = "id")]
public required string Id { get; init; }
///
/// 密码
///
[Required]
+ [FromForm(Name = "password")]
public required string Password { get; init; }
}
@@ -51,15 +54,24 @@ public record UpdateParameter
/// 用户名
///
[Required]
+ [FromForm(Name = "name")]
public required string UserName { get; init; }
///
/// 邮箱
///
+ [FromForm(Name = "email")]
public string? Email { get; init; }
///
/// 联系电话
///
+ [FromForm(Name = "mobile")]
public string? Mobile { get; init; }
+
+ ///
+ /// 用户头像
+ ///
+ [FromForm(Name = "avatar")]
+ public IFormFile? Avatar { get; init; }
}
diff --git a/Server/Dockerfile b/Server/Dockerfile
index a4907fc..aa3a84d 100644
--- a/Server/Dockerfile
+++ b/Server/Dockerfile
@@ -1,8 +1,14 @@
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:7.0
+RUN apt-get update && apt-get install -y libfontconfig1
COPY . /app
WORKDIR /app
-EXPOSE 80
+EXPOSE 5180
+
+ARG UID=1026
+ARG GID=100
+RUN useradd -m -u ${UID} tsanie
+USER ${UID}:${GID}
ENTRYPOINT ["dotnet", "Server.dll"]
\ No newline at end of file
diff --git a/Server/Program.cs b/Server/Program.cs
index 1f1dd24..6d69172 100644
--- a/Server/Program.cs
+++ b/Server/Program.cs
@@ -11,7 +11,14 @@ public class Program
///
public const string ProjectName = "Flower Story";
///
- public const string Version = "1.0.807";
+ public const string Version = "1.1.808";
+ ///
+ public static string DataPath =>
+#if DEBUG
+ AppDomain.CurrentDomain.BaseDirectory;
+#else
+ "/data";
+#endif
///
public static void Main(string[] args)
@@ -56,7 +63,9 @@ public class Program
options.IncludeXmlComments(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Server.xml"), true);
});
- builder.Services.AddDbContext(options => options.UseSqlite("DataSource=flower.db;Cache=Shared"));
+ var dbPath = Path.Combine(DataPath, "flower.db");
+ builder.Services.AddDbContext(options => options.UseSqlite($"DataSource={dbPath};Cache=Shared"));
+
builder.Services.AddScoped();
var app = builder.Build();
diff --git a/Server/Server.csproj b/Server/Server.csproj
index 577db82..3938a61 100644
--- a/Server/Server.csproj
+++ b/Server/Server.csproj
@@ -10,6 +10,14 @@
README.md
+
+
+
+
+
+
+
+
@@ -18,6 +26,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
+
@@ -25,6 +34,9 @@
PreserveNewest
+
+ PreserveNewest
+
Never
diff --git a/Server/appsettings.json b/Server/appsettings.json
index 10f68b8..c8f253b 100644
--- a/Server/appsettings.json
+++ b/Server/appsettings.json
@@ -1,9 +1,16 @@
{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "Kestrel": {
+ "Endpoints": {
+ "Http": {
+ "Url": "http://+:5180"
+ }
+ }
}
- },
- "AllowedHosts": "*"
}
diff --git a/Server/start b/Server/start
new file mode 100644
index 0000000..eee66d9
--- /dev/null
+++ b/Server/start
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+docker stop flower-server
+docker rm flower-server
+docker rmi flower-server-image
+
+docker build -t flower-server-image .
+docker run -d -p 7080:5180 -v /volume2/docker/flower-data:/data --name flower-server flower-server-image:latest
+
+docker logs -f flower-server
\ No newline at end of file
diff --git a/Server/wwwroot/image/avatar.jpg b/Server/wwwroot/image/avatar.jpg
new file mode 100644
index 0000000..5da644c
Binary files /dev/null and b/Server/wwwroot/image/avatar.jpg differ