MinIO + ImageSharp + Swagger实现自搭对象存储和图片处理

MinIO + ImageSharp + Swagger实现自搭对象存储和图片处理

文章目录

  • MinIO + ImageSharp + Swagger实现自搭对象存储和图片处理
    • MinIO搭建
    • 准备工作
    • 新建项目
      • 新建config.yaml
      • 新建NLog.config
      • 新建App.cs
      • 修改Program.cs
      • 修改Startup.cs
    • 接口编写
      • 文件上传
      • 图片下载(浏览)
      • 文件删除
    • 运行效果
      • 上传图像
      • 浏览图像
    • Last
    • 示例项目下载地址

MinIO搭建

参考之前的文章,不多叙述
传送门:Windows Server搭建MinIO快速指南

准备工作

  • Visual Studio 2019
  • PostMan
  • Notepad++

新建项目

选择项目:ASP.NET Core Web API
MinIO + ImageSharp + Swagger实现自搭对象存储和图片处理_第1张图片
MinIO + ImageSharp + Swagger实现自搭对象存储和图片处理_第2张图片

勾选OpenAPI支持
MinIO + ImageSharp + Swagger实现自搭对象存储和图片处理_第3张图片

新建好项目后,打开工具 -> Nuget
MinIO + ImageSharp + Swagger实现自搭对象存储和图片处理_第4张图片

安装如下几个库

  • Chloe:Install-Package Chloe
  • Chloe.Extension:Install-Package Chloe.Extension
  • Chloe.SQLite:Install-Package Chloe.SQLite

Chloe是ORM框架,这里用SQLite数据库来记录文件上传记录及文件相关信息

  • Microsoft.Data.Sqlite.Core:Install-Package Microsoft.Data.Sqlite.Core
  • Minio:Install-Package Minio

操作MinIO对象存储的SDK

  • Newtonsoft.Json:Install-Package Newtonsoft.Json

Json序列化和反序列化库

  • NLog:Install-Package NLog

日志记录

  • Quartz:Install-Package Quartz

定时任务框架,用来定时清理临时文件

  • SharpYaml:Install-Package SharpYaml

用来配置系统各项参数,使用yml作为系统配置文件

  • SixLabors.ImageSharp:Install-Package SixLabors.ImageSharp

Image处理框架

  • Swashbuckle.AspNetCore.Annotations:Install-Package Swashbuckle.AspNetCore.Annotations

Swagger注解库

  • System.Data.SQLite.Core:Install-Package System.Data.SQLite.Core

SQLite官方驱动

目录框架如下:
MinIO + ImageSharp + Swagger实现自搭对象存储和图片处理_第5张图片

新建config.yaml

Databases:
  SQLite:
    Path: daka.data.db

MinIO:
  EndPoint: 127.0.0.1:9001
  APIEndPoint: 127.0.0.1:9000
  UseSsl: false
  FileBucket: file
  ImageBucket: image
  ReportBucket: report
  AccessKey: dakapath
  SecertKey: 123456

System:
  Title: 杭州大伽文件服务接口系统
  Company: 杭州大伽信息科技有限公司
  TempPath: Temp
  TempCleanJobCron: 0 0/1 * * * ?

新建NLog.config

<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <targets>
    
    
    <target name="info" xsi:type="ColoredConsole"
            layout="${longdate} | ${uppercase:${level}} | ${message}" />

    
    
    <target name="debug" xsi:type="Debugger"
            layout="${longdate} | ${uppercase:${level}} | ${message}" />

    
    <target name="error" xsi:type="File"
            fileName="${basedir}/Logs/${shortdate}-Error.txt"
            maxArchiveFiles="100"
            layout="${longdate} | ${uppercase:${level}} | ${message}" />
    <target name="trace" xsi:type="File"
            fileName="${basedir}/Logs/${shortdate}-Trace.txt"
            maxArchiveFiles="100"
            layout="${longdate} | ${uppercase:${level}} | ${message}" />
    <target name="fatal" xsi:type="File"
            fileName="${basedir}/Logs/${shortdate}-Fatal.txt"
            maxArchiveFiles="100"
            layout="${longdate} | ${uppercase:${level}} | ${message}" />
  targets>
  <rules>
    
    <logger name="*" minlevel="Debug" writeTo="debug" />
    <logger name="*" minlevel="Error" writeTo="error" />
    <logger name="*" minlevel="Info" writeTo="info" />
    <logger name="*" minlevel="Trace" writeTo="trace" />
    <logger name="*" minlevel="Fatal" writeTo="fatal" />
  rules>
nlog>

新建App.cs

using Chloe;
using Chloe.SQLite;
using DakaPathAppFileService.ConfigModels;
using DakaPathAppFileService.DataHelper;
using DakaPathAppFileService.ExtendMethod;
using NLog;
using SharpYaml.Serialization;
using System;
using System.IO;

namespace DakaPathAppFileService
{
    public class App
    {
        static readonly Logger logger = LogManager.GetCurrentClassLogger();
        private static string SQLiteConnectionString;

        public static string MinIOEndPoint;
        public static string MinIOAPIEndPoint;
        public static bool? MinIOUseSsl;
        public static string MinIOFileBucket;
        public static string MinIOImageBucket;
        public static string MinIOReportBucket;
        public static string MinIOAccessKey;
        public static string MinIOSecertKey;

        public static string SystemTitle;
        public static string SystemCompany;
        public static string SystemTempPath;
        public static string SystemTempCleanJobCron;
        public static int YearTime => DateTime.Now.Year;

        public static DbContext sqlite
        {
            get
            {
                return SQLiteConnectionString.IsBlank() ? null : new SQLiteContext(new SQLiteConnectionFactory(SQLiteConnectionString));
            }
        }

        public static void ReadSystemConfig()
        {
            try
            {
                var serializer = new Serializer();
                string configConfigPath = Path.Combine(System.AppDomain.CurrentDomain.SetupInformation.ApplicationBase, "config.yml");
                if (configConfigPath.IsBlank())
                {
                    return;
                }
                if (!File.Exists(configConfigPath))
                {
                    return;
                }
                using (Stream f = new FileStream(configConfigPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                {
                    DakaPathAppFileServiceYamlConfigModel Config = serializer.Deserialize<DakaPathAppFileServiceYamlConfigModel>(f);
                    SQLiteConnectionString = Config.Databases.SQLite.GetConnectionString();

                    MinIOEndPoint = Config.MinIO.EndPoint;
                    MinIOAPIEndPoint = Config.MinIO.APIEndPoint;
                    MinIOUseSsl = Config.MinIO.UseSsl;
                    MinIOFileBucket = Config.MinIO.FileBucket;
                    MinIOImageBucket = Config.MinIO.ImageBucket;
                    MinIOReportBucket = Config.MinIO.ReportBucket;
                    MinIOAccessKey = Config.MinIO.AccessKey;
                    MinIOSecertKey = Config.MinIO.SecertKey;

                    SystemTitle = Config.System.Title;
                    SystemCompany = Config.System.Company;
                    SystemTempPath = Config.System.TempPath;
                    SystemTempCleanJobCron = Config.System.TempCleanJobCron;
                }
                logger.Trace($"配置载入完毕!");
            }
            catch (Exception e)
            {
                logger.Error($"配置载入失败: {e}");
            }
        }

        public App()
        {
            var logoDirPath = System.AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
            NLog.GlobalDiagnosticsContext.Set("LogPath", logoDirPath);
        }
    }
}

修改Program.cs

Program中添加配置载入入口和定时任务的触发

using DakaPathAppFileService.Jobs;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Quartz;
using Quartz.Impl;

namespace DakaPathAppFileService
{
    public class Program
    {
        public static void Main(string[] args)
        {
            App.ReadSystemConfig();

            IScheduler scheduler;
            ISchedulerFactory factory;
            factory = new StdSchedulerFactory();
            scheduler = factory.GetScheduler().Result;
            scheduler.Start().Wait();
            IJobDetail job = JobBuilder.Create<TempCleanJob>().WithIdentity("TempCleanJob", "Group").Build();
            ITrigger trigger = TriggerBuilder.Create()
                .WithIdentity("TempCleanJob", "Group")
                .WithCronSchedule(App.SystemTempCleanJobCron)
                .Build();
            scheduler.ScheduleJob(job, trigger).Wait();
            scheduler.Start().Wait();

            CreateHostBuilder(args).Build().Run();

            scheduler.Shutdown();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

修改Startup.cs

Startup中添加Swagger相关信息
MinIO + ImageSharp + Swagger实现自搭对象存储和图片处理_第6张图片

接口编写

Put、Delete、Get三种Http请求分别对应接口的增删查

  • Produces注解是声明返回的Content-Type类型
  • SwaggerParameter注解是解释参数的意义
  • SignTimeStamp用来验证接口请求是否合法,简单加强接口安全性

文件上传

文件上传接口代码如下:

[HttpPut, Route("upload"), Produces("application/json")]
public async Task<object> Upload([SwaggerParameter("文件")] IFormFile file
    , [SwaggerParameter("Sign")] string sign
    , [SwaggerParameter("TimeStamp")] long? t)
{
    dynamic json = new ExpandoObject();
    json.data = null;
    if (sign.IsBlank() || !t.HasValue)
    {
        json.code = 1;
        json.msg = $"参数异常";
        return json;
    }
    if (t.Value <= 0)
    {
        json.code = 1;
        json.msg = $"接口请求超时";
        return json;
    }
    var dis = Math.Abs(DateTime.Now.Subtract(t.Value.TimeStampToDateTime().Value).TotalSeconds);
    if (dis > 5)
    {
        json.code = 1;
        json.msg = $"接口请求超时";
        return json;
    }
    var _sign = $"LONG.{t}AT.STRING".MD5Encrypt32().ToUpper();
    if (sign != _sign)
    {
        json.code = 1;
        json.msg = $"签名验证失败";
        return json;
    }
    try
    {
        MinioClient mc = new MinioClient(App.MinIOAPIEndPoint, App.MinIOAccessKey, App.MinIOSecertKey);
        var fileSaveName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
        var filePath = $"http{(App.MinIOUseSsl == true ? "s" : "")}://{App.MinIOEndPoint}/{App.MinIOImageBucket}/{fileSaveName}";
        await mc.PutObjectAsync(App.MinIOImageBucket, fileSaveName, file.OpenReadStream(), file.Length);
        DakaFile dakaFile = new DakaFile
        {
            BucketName = App.MinIOImageBucket,
            SaveName = fileSaveName,
            FileName = file.FileName,
            FilePath = filePath,
            FileSize = file.Length,
            FileExtension = Path.GetExtension(file.FileName),
            CteateTime = DateTime.Now,
            UpdateTime = DateTime.Now
        };
        using (DbContext my = App.sqlite)
        {
            my.Insert(dakaFile);
        }
        dynamic _file = new ExpandoObject();
        _file.Id = dakaFile.Id;
        _file.FilePath = filePath;
        json.code = 0;
        json.msg = $"上传成功";
        json.data = _file;
        return json;
    }
    catch (Exception e)
    {
        logger.Error($"{e}");
        json.code = 1;
        json.msg = $"{e}";
        json.data = null;
        return json;
    }
}

图片下载(浏览)

图片浏览和下载使用同一个接口实现,通过参数preview判断是下载还是浏览,代码如下:

[SwaggerOperation(
    Summary = "图片下载",
    Description = "图片下载接口,可以证据特定参数,实现图片的缩放、旋转。缩放时如果同时传了比例和高宽度参数,优先根据比例参数。",
    Tags = new[] { "图片相关接口" }
)]
[HttpGet, Route("download")]
public async Task<IActionResult> Download([SwaggerParameter("图片ID")] int? id
    , [SwaggerParameter("是否预览")] bool? preview
    , [SwaggerParameter("是否缩放")] bool? resize
    , [SwaggerParameter("是否旋转")] bool? rotate
    , [SwaggerParameter("旋转方向:0(或非1值).逆时针;1.顺时针")] int? rotate_d
    , [SwaggerParameter("按比例缩放")] double? resize_p
    , [SwaggerParameter("按高度缩放")] int? resize_h
    , [SwaggerParameter("按宽度缩放")] int? resize_w
    , [SwaggerParameter("旋转度数")] int? rotate_degree
    , [SwaggerParameter("Sign")] string sign
    , [SwaggerParameter("TimeStamp")] long? t)
{
    dynamic json = new ExpandoObject();
    json.data = null;
    if (sign.IsBlank() || !t.HasValue)
    {
        json.code = 1;
        json.msg = $"参数异常";
        return Content(JsonConvert.SerializeObject(json), "application/json", Encoding.UTF8);
    }
    if (t.Value <= 0)
    {
        json.code = 1;
        json.msg = $"接口请求超时";
        return Content(JsonConvert.SerializeObject(json), "application/json", Encoding.UTF8);
    }
    var dis = Math.Abs(DateTime.Now.Subtract(t.Value.TimeStampToDateTime().Value).TotalSeconds);
    if (dis > 5)
    {
        json.code = 1;
        json.msg = $"接口请求超时";
        return Content(JsonConvert.SerializeObject(json), "application/json", Encoding.UTF8);
    }
    var _sign = $"LONG.{t}AT.STRING".MD5Encrypt32().ToUpper();
    if (sign != _sign)
    {
        json.code = 1;
        json.msg = $"签名验证失败";
        return Content(JsonConvert.SerializeObject(json), "application/json", Encoding.UTF8);
    }
    DakaImage dakaImage = null;
    using (DbContext my = App.sqlite)
    {
        dakaImage = my.QueryByKey<DakaImage>(id);
    }
    if (dakaImage == null)
    {
        json.code = 1;
        json.msg = $"未找到相关图片信息";
        return Content(JsonConvert.SerializeObject(json), "application/json", Encoding.UTF8);
    }
    if (dakaImage.IsDelete == true)
    {
        json.code = 1;
        json.msg = $"图片已被删除";
        return Content(JsonConvert.SerializeObject(json), "application/json", Encoding.UTF8);
    }
    try
    {
        MinioClient mc = new MinioClient(App.MinIOAPIEndPoint, App.MinIOAccessKey, App.MinIOSecertKey);
        string filepath = Path.Combine(App.SystemTempPath, dakaImage.SaveName);
        string new_iamge_filepath = Path.Combine(App.SystemTempPath, $"{Guid.NewGuid()}{dakaImage.FileExtension}");
        string dirpath = Path.GetDirectoryName(filepath);
        if (!Directory.Exists(dirpath))
        {
            Directory.CreateDirectory(dirpath);
        }
        await mc.GetObjectAsync(App.MinIOImageBucket, dakaImage.SaveName, (x =>
        {
            using (FileStream fs = new FileStream(filepath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read))
            {
                x.CopyTo(fs);
                fs.Flush();
                fs.Dispose();
            }
            x.Dispose();
        }));
        using (Image image = Image.Load(filepath)) 
        {
            if (image != null)
            {
                bool changed = false;
                if (resize == true)
                {
                    if (resize_p != null)
                    {
                        if (resize_p.Value > 2)
                        {
                            resize_p = 2d;
                        }
                        image.Mutate(x =>
                        {
                            x.Resize((int)(image.Width * resize_p.Value), (int)(image.Height * resize_p.Value));
                        });
                        changed = true;
                    }
                    else
                    {
                        if (resize_h.HasValue)
                        {
                            image.Mutate(x =>
                            {
                                x.Resize(image.Width, resize_h.Value);
                            });
                            changed = true;
                        }
                        if (resize_w.HasValue)
                        {
                            image.Mutate(x =>
                            {
                                x.Resize(resize_w.Value, image.Height);
                            });
                            changed = true;
                        }
                    }
                }
                if (rotate == true)
                {
                    if (rotate_d == 1)
                    {
                        image.Mutate(x =>
                        {
                            x.Rotate(rotate_degree.Value);
                        });
                        changed = true;
                    }
                    else
                    {
                        image.Mutate(x =>
                        {
                            x.Rotate(360f - rotate_degree.Value);
                        });
                        changed = true;
                    }
                }
                if (changed)
                {
                    image.Save(new_iamge_filepath);
                    filepath = new_iamge_filepath;
                }
            }
        }
        if (preview == true)
        {
            return File(System.IO.File.ReadAllBytes(filepath), "image/jpeg");
        }
        return File(System.IO.File.ReadAllBytes(filepath), "application/octet-stream", dakaImage.SaveName);
    }
    catch (Exception e)
    {
        logger.Error($"{e}");
        json.code = 1;
        json.msg = $"{e}";
        return Content(JsonConvert.SerializeObject(json), "application/json", Encoding.UTF8);
    }
}

文件删除

文件删除接口代码如下:

[HttpDelete, Route("delete"), Produces("application/json")]
public async Task<object> Delete([SwaggerParameter("图片ID")] int? id
    , [SwaggerParameter("Sign")] string sign
    , [SwaggerParameter("TimeStamp")] long? t)
{
    dynamic json = new ExpandoObject();
    json.data = null;
    if (sign.IsBlank() || !t.HasValue)
    {
        json.code = 1;
        json.msg = $"参数异常";
        return json;
    }
    if (t.Value <= 0)
    {
        json.code = 1;
        json.msg = $"接口请求超时";
        return json;
    }
    var dis = Math.Abs(DateTime.Now.Subtract(t.Value.TimeStampToDateTime().Value).TotalSeconds);
    if (dis > 5)
    {
        json.code = 1;
        json.msg = $"接口请求超时";
        return json;
    }
    var _sign = $"LONG.{t}AT.STRING".MD5Encrypt32().ToUpper();
    if (sign != _sign)
    {
        json.code = 1;
        json.msg = $"签名验证失败";
        return json;
    }
    DakaImage dakaImage = null;
    using (DbContext my = App.sqlite)
    {
        dakaImage = my.QueryByKey<DakaImage>(id);
    }
    if (dakaImage == null)
    {
        json.code = 1;
        json.msg = $"未找到相关图片信息";
        return json;
    }
    if (dakaImage.IsDelete == true)
    {
        json.code = 1;
        json.msg = $"图片已被删除";
        return json;
    }
    try
    {
        MinioClient mc = new MinioClient(App.MinIOAPIEndPoint, App.MinIOAccessKey, App.MinIOSecertKey);
        await mc.RemoveObjectAsync(App.MinIOImageBucket, dakaImage.SaveName);
        using (DbContext my = App.sqlite)
        {
            dakaImage.IsDelete = true;
            my.UpdateOnly<DakaImage>(dakaImage, a => new { a.IsDelete });
        }
        json.code = 0;
        json.msg = $"删除成功";
        return json;
    }
    catch (Exception e)
    {
        logger.Error($"{e}");
        json.code = 1;
        json.msg = $"{e}";
        return json;
    }
}

运行效果

为了方便截图,这里先把Sign验证关闭了
MinIO + ImageSharp + Swagger实现自搭对象存储和图片处理_第7张图片

上传图像

上传成功后会返回MinIO的直接访问地址,但是走接口的话,无法使用,可以屏蔽data里的链接

浏览图像

可以根据参数对图像进行简单的处理

  • 浏览原图

  • 浏览50%缩放图

  • 20%缩放并顺时针旋转108度,Swagger浏览效果不太好,所以这里用PostMan
    MinIO + ImageSharp + Swagger实现自搭对象存储和图片处理_第8张图片

  • 下载图像
    MinIO + ImageSharp + Swagger实现自搭对象存储和图片处理_第9张图片

Last

  • 整个项目使用MinIO做分布式对象存储系统,并使用Asp.Core封装使用接口对外开放上传、删除、下载等权限
  • 项目使用Swagger作为接口测试工具,仅在Debug模式下可访问Swagger界面并调试接口,Release发布模式下无法访问Swagger和任何界面
    MinIO + ImageSharp + Swagger实现自搭对象存储和图片处理_第10张图片
  • 使用ImageSharp做为图像处理工具,可以脱离System.Drawing,方便快捷的对图像进行简易处理
  • 使用SQLite进行记录存储,方便接口系统迁移
  • 使用Chloe作为数据库ORM框架
  • 系统配置使用了通用的Yaml格式
  • 使用了Quartz作为系统定时任务执行框架,根据配置文件可以自行修改Cron表达式实现定时清理缓存文件

示例项目下载地址

代码中Sign加密格式已做更改
杭州大伽文件服务接口系统:DakaPathAppFileService

你可能感兴趣的:(C#,MinIO,Swagger,ImageSharp,对象存储,图像处理)