曦远 (DealiAxy)一份私人的文字刊物

StarBlog
返回文章列表折页手记阅读全文
StarBlog博客开发笔记

StarBlog Blog Development Notes (25): Image APIs and File Upload

Preface There are two styles for designing file-upload APIs. One is to set up a single upload endpoint for the entire project, and everywhere else that needs files only stores a reference ID. The other is for each place that needs files to manage its own files separately. Both have their pros and cons. This project chose the latter style: the article images and photo modules need both CRUD and batch import, so managing files separately is better. Image APIs Back to the main topic—first, an introduction to the image-related APIs. Image list CRUD is a given, and paginated viewing of the image list is also available, but because

曦远 (DealiAxy)约 26 分钟
目录8 个章节

Preface

There are two styles for designing file-upload APIs. One is to set up a single upload endpoint for the entire project, and everywhere else that needs files only stores a reference ID. The other is for each place that needs files to manage its own files separately. Both have their pros and cons. This project chose the latter style: the article images and photo modules need both CRUD and batch import, so managing files separately is better.

Image APIs

Back to the main topic—first, an introduction to the image-related APIs.

Image List

CRUD is a given, and paginated viewing of the image list is also available. However, since filtering isn’t implemented, I didn’t define a ViewModel as a parameter.

Controller code StarBlog.Web/Apis/Blog/PhotoController.cs

[HttpGet]
public ApiResponsePaged<Photo> GetList(int page = 1, int pageSize = 10) {
    var paged = _photoService.GetPagedList(page, pageSize);
    return new ApiResponsePaged<Photo> {
        Pagination = paged.ToPaginationMetadata(),
        Data = paged.ToList()
    };
}

It shares the same image-list logic with the blog frontend, so this part was extracted into a service. The code is as follows

StarBlog.Web/Services/PhotoService.cs

public IPagedList<Photo> GetPagedList(int page = 1, int pageSize = 10) {
    return _photoRepo.Select.OrderByDescending(a => a.CreateTime)
        .ToList().ToPagedList(page, pageSize);
}

Single Image

Getting a single image is similar to getting an article: pass in an ID; if it’s not found, return 404; if it is found, return the image object

[HttpGet("{id}")]
public ApiResponse<Photo> Get(string id) {
    var photo = _photoService.GetById(id);
    return photo == null
        ? ApiResponse.NotFound($"图片 {id} 不存在")
        : new ApiResponse<Photo> {Data = photo};
}

Image Thumbnails

In part 20 of this series, this project already implemented image display optimization. See: Developing a Blog Project with .NetCore: StarBlog - (20) Image Display Optimization

Besides the thumbnail functionality provided by the ImageSharp component, I also wrote another method for generating thumbnails. This method has two characteristics

  • It is generated and returned directly in memory, without writing a cache file
  • It generates Progressive JPEG format, which ImageSharp currently does not support, and can improve frontend loading speed

Controller code

[HttpGet("{id}/Thumb")]
public async Task<IActionResult> GetThumb(string id, [FromQuery] int width = 300) {
    var data = await _photoService.GetThumb(id, width);
    return new FileContentResult(data, "image/jpeg");
}

Service code

/// <summary>
/// 生成Progressive JPEG缩略图 (使用 MagickImage)
/// </summary>
/// <param name="width">设置为0则不调整大小</param>
public async Task<byte[]> GetThumb(string id, int width = 0) {
    var photo = await _photoRepo.Where(a => a.Id == id).FirstAsync();
    using (var image = new MagickImage(GetPhotoFilePath(photo))) {
        image.Format = MagickFormat.Pjpeg;
        if (width != 0) {
            image.Resize(width, 0);
        }

        return image.ToByteArray();
    }
}

This MagickImage is written in C++ and references different native libraries on different platforms. You need to add configuration in the csproj so that the corresponding dependency libraries are included when publishing. And it seems there are pitfalls on CentOS…

<!--  复制 Magick 库  -->
<PropertyGroup>
    <MagickCopyNativeWindows>true</MagickCopyNativeWindows>
    <MagickCopyNativeLinux>true</MagickCopyNativeLinux>
    <MagickCopyNativeMacOS>true</MagickCopyNativeMacOS>
</PropertyGroup>

Other APIs

There are a few more APIs that are more or less the same as those introduced earlier. Repeating them wouldn’t add much; readers can check the source code if needed.

Image File Upload

This is also the add-image API

First, define the DTO

public class PhotoCreationDto {
    /// <summary>
    /// 作品标题
    /// </summary>
    [Required(ErrorMessage = "作品标题不能为空")]
    public string Title { get; set; }

    /// <summary>
    /// 拍摄地点
    /// </summary>
    [Required(ErrorMessage = "拍摄地点不能为空")]
    public string Location { get; set; }
}

Controller code

[Authorize]
[HttpPost]
public ApiResponse<Photo> Add([FromForm] PhotoCreationDto dto, IFormFile file) {
    var photo = _photoService.Add(dto, file);

    return !ModelState.IsValid
        ? ApiResponse.BadRequest(ModelState)
        : new ApiResponse<Photo>(photo);
}

Because some extra data needs to be sent along with the upload, parameters must be passed via FormData, so the [FromForm] attribute is used to mark this dto parameter

A parameter of type IFormFile can be used to get the uploaded file

Below is the service code

public Photo Add(PhotoCreationDto dto, IFormFile photoFile) {
    var photoId = GuidUtils.GuidTo16String();
    var photo = new Photo {
        Id = photoId,
        Title = dto.Title,
        CreateTime = DateTime.Now,
        Location = dto.Location,
        FilePath = Path.Combine("photography", $"{photoId}.jpg")
    };

    var savePath = Path.Combine(_environment.WebRootPath, "media", photo.FilePath);
	
    // 如果超出最大允许的大小,则按比例缩小
    const int maxWidth = 2000;
    const int maxHeight = 2000;
    using (var image = Image.Load(photoFile.OpenReadStream())) {
        if (image.Width > maxWidth)
            image.Mutate(a => a.Resize(maxWidth, 0));
        if (image.Height > maxHeight)
            image.Mutate(a => a.Resize(0, maxHeight));
        image.Save(savePath);
    }

    // 保存文件
    using (var fs = new FileStream(savePath, FileMode.Create)) {
        photoFile.CopyTo(fs);
    }

    // 读取图片的尺寸等数据
    photo = BuildPhotoData(photo);

    return _photoRepo.Insert(photo);
}

Some processing is done on the image here. Setting those details aside, for the uploaded file the most critical part is really just a few lines of save code

using (var fs = new FileStream("savePath", FileMode.Create)) {
    photoFile.CopyTo(fs);
}

This completes the file upload API.

Series Articles

_EOF_

StarBlog Blog Development Notes (25): Image APIs and File Upload

本文作者:
程序设计实验室
版权声明:
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

相关文章

StarBlog博客开发笔记(1):项目介绍

前言 OK,我也来造轮子了 博客系统从一开始用WordPress,再到后来用hexo、hugo之类的静态博客生成放github托管,一直在折腾 折腾是为了更好解决问题,最终还是打算自己花时间搞一个好了 本系列文章将记录博客的开发过程~ 后端基于最新的.Net6和Asp.Net Core框架,遵循RESTFul接口规范,前端基于Vue+ElementUI开发,可作为 .Net Core 入门项目学习

2022-04-05 · StarBlog博客开发笔记 · 约 1 分钟

讨论

共 0 条

良言一句三冬暖,恶语伤人六月寒。

加载评论中…