返回文章列表文章索引阅读全文
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博客开发笔记(23):文章列表接口分页、过滤、搜索、排序

前言 上一篇留的坑,火速补上。 在之前的第6篇中,已经有初步介绍,本文做一些补充,已经搞定这部分的同学可以快速跳过,基于.NetCore开发博客项目 StarBlog - (6) 页面开发之博客文章列表 对标准的WebApi来说,分页、过滤、搜索、排序是很常见的功能,既可以方便用户查看数据,又可以提升程序性能。 通用请求参数 定义一个类来作为通用的请求参数 列表接口通用的参数是这几个:PageSi

2023-01-02 · StarBlog博客开发笔记 · 约 1 分钟

讨论

共 0 条

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

加载评论中…